在Flask和PostgreSQL中使用SQLAlchemy

Using SQLAlchemy with Flask and PostgreSQL

介绍

数据库是现代应用程序中至关重要的部分,因为它们存储了用于为其提供动力的数据。 通常,我们使用结构化查询语言(SQL)对数据库执行查询并处理其中的数据。 尽管最初是通过专用的SQL工具完成的,但是我们已经迅速转移到在应用程序内部使用SQL来执行查询。

自然地,随着时间的流逝,对象关系映射器(ORM)诞生了,这使我们能够安全,轻松,方便地以编程方式连接到我们的数据库,而无需实际运行查询来处理数据。

一种这样的ORM是SQLAlchemy。 在本文中,我们将更深入地研究ORM,尤其是SQLAlchemy,然后使用它来使用Flask框架来构建数据库驱动的Web应用程序。

什么是ORM,为什么要使用它?

顾名思义,对象关系映射将对象映射到关系实体。 在面向对象的编程语言中,对象与关系实体没有什么不同-它们具有可以互换地映射的某些字段/属性。

话虽如此,因为将对象映射到数据库相当容易,所以反向操作也非常简单。 这简化了软件开发过程,并减少了在编写普通SQL代码时出现人为错误的机会。

使用ORM的另一个优点是,通过允许我们使用模型来操纵数据,而不是每次需要访问数据库时都编写SQL代码,它们可以帮助我们编写符合DRY(不要重复自己)原则的代码。

ORM从我们的应用程序中提取数据库,使我们能够轻松使用多个数据库或切换数据库。 假设,如果我们在应用程序中使用SQL连接到MySQL数据库,则由于要更改其语法,因此如果要切换到MSSQL数据库,则需要修改代码。

如果我们的SQL在我们的应用程序中的多个点集成在一起,这将被证明很麻烦。 通过ORM,我们需要进行的更改将仅限于更改几个配置参数。

尽管ORM通过抽象化数据库操作使我们的生活更轻松,但我们仍需注意不要忘记幕后发生的事情,因为这也将指导我们如何使用ORM。 我们还需要熟悉ORM并学习它们,以便更有效地使用它们,这会引入一些学习曲线。

SQLAlchemy ORM

SQLAlchemy是用Python编写的ORM,可为开发人员提供SQL的功能和灵活性,而无需真正使用它。

SQLAlchemy环绕了Python附带的Python数据库API(Python DBAPI),其创建目的是为了促进Python模块与数据库之间的交互。

创建DBAPI是为了建立数据库管理的一致性和可移植性,尽管我们不需要直接与它进行交互,因为SQLAlchemy将是我们的联系点。

还需要注意的是,SQLAlchemy ORM是建立在SQLAlchemy Core之上的-SQLAlchemy Core处理DBAPI集成并实现SQL。 换句话说,SQLAlchemy Core提供了生成SQL查询的方法。

尽管SQLAlchemy ORM使我们的应用程序与数据库无关,但必须注意,特定的数据库将需要特定的驱动程序才能连接到它们。 一个很好的例子是Pyscopg,它是DBAPI的PostgreSQL实现,当与SQLAlchemy结合使用时,它允许我们与Postgres数据库进行交互。

对于MySQL数据库,PyMySQL库提供了与之交互所需的DBAPI实现。

SQLAlchemy也可以与Oracle和Microsoft SQL Server一起使用。 行业中一些依赖SQLAlchemy的知名企业包括Reddit,Yelp,DropBox和Survey Monkey。

介绍了ORM之后,让我们构建一个简单的Flask API,该API与Postgres数据库进行交互。

带SQLAlchemy的烧瓶

Flask是一个轻量级的微框架,用于构建最少的Web应用程序,并且通过第三方库,我们可以利用其灵活性来构建健壮且功能丰富的Web应用程序。

在我们的案例中,我们将构建一个简单的RESTful API,并使用Flask-SQLAlchemy扩展将我们的API连接到Postgres数据库。

先决条件

我们将使用PostgreSQL(也称为Postgres)来存储将由API处理和操纵的数据。

要与我们的Postgres数据库进行交互,我们可以使用命令行或带有图形用户界面的客户端,使它们更易于使用并且导航更快。

对于Mac OS,我建议您使用Postico,它非常简单直观,并提供干净的用户界面。

PgAdmin是另一个出色的客户端,支持所有主要操作系统,甚至提供Dockerized版本。

我们将使用这些客户端来创建数据库,并在应用程序的开发和执行期间查看数据。

随着安装的进行,让我们创建环境并安装应用程序所需的依赖项:

1
2
3
4
5
$ virtualenv --python=python3 env --no-site-packages
$ source env/bin/activate
$ pip install psycopg2-binary
$ pip install flask-sqlalchemy
$ pip install Flask-Migrate

上面的命令将创建并激活virtualenv,安装Psycopg2驱动程序,安装flask-sqlalchemy,并安装Flask-Migrate来处理数据库迁移。

Flask-Migrate使用Alembic,这是一个轻量级的数据库迁移工具,可通过帮助我们创建和重新创建数据库,将数据移入和移入数据库以及识别数据库状态来帮助我们以更清晰的方式与数据库交互。

在我们的情况下,我们不必每次应用程序启动时都重新创建数据库或表,并且在不存在的情况下将自动为我们执行此操作。

实作

我们将构建一个简单的API来处理和操纵有关汽车的信息。 数据将存储在PostgreSQL数据库中,并且通过API我们将执行CRUD操作。

首先,我们必须使用我们选择的PostgreSQL客户端创建cars_api数据库:

sqlalchemy_create_db

放置好数据库后,让我们连接它。 我们将从在apps.py文件中引导我们的Flask API开始:

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return {"hello":"world"}

if __name__ == '__main__':
    app.run(debug=True)

我们首先创建一个Flask应用程序和一个返回JSON对象的端点。

对于我们的演示,我们将使用Flask-SQLAlchemy,这是专门用于向Flask应用程序添加SQLAlchemy功能的扩展。

现在让我们将Flask-SQLAlchemy和Flask-Migrate集成到我们的app.py中,并创建一个模型,该模型将定义有关将存储的汽车的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Previous imports remain...
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] ="postgresql://postgres:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d1a1bea2a5b6a3b4a291bdbeb2b0bdb9bea2a5">[emailprotected]</a>:5432/cars_api"
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class CarsModel(db.Model):
    __tablename__ = 'cars'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String())
    model = db.Column(db.String())
    doors = db.Column(db.Integer())

    def __init__(self, name, model, doors):
        self.name = name
        self.model = model
        self.doors = doors

    def __repr__(self):
        return f"<Car {self.name}>"

导入flask_sqlalchemy之后,我们首先将数据库URI添加到应用程序的配置中。 该URI包含我们的凭据,服务器地址以及将用于我们的应用程序的数据库。

然后,我们创建一个名为db的Flask-SQLAlchemy实例,并将其用于所有数据库交互。 之后,创建了名为migrate的Flask-Migrate实例,该实例将用于处理我们项目的迁移。

CarsModel是将用于定义和处理数据的模型类。 该类的属性表示我们要存储在数据库中的字段。

我们通过在包含数据的列旁边使用__tablename__来定义表的名称。

Flask带有命令行界面和专用命令。 例如,要启动我们的应用程序,我们使用命令flask run。 要使用此脚本,我们只需要定义一个环境变量即可指定托管Flask应用程序的脚本:

1
2
3
4
5
6
7
8
9
$ export FLASK_APP=app.py
$ flask run
 * Serving Flask app"app.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 172-503-577

放置好模型并集成Flask-Migrate后,让我们使用它在数据库中创建cars表:

1
2
3
$ flask db init
$ flask db migrate
$ flask db upgrade

我们首先初始化数据库并启用迁移。 生成的迁移只是用于定义要在我们的数据库上执行的操作的脚本。 由于这是第一次,因此脚本将只生成带有模型中指定列的cars表。

flask db upgrade命令执行迁移并创建我们的表:

sqlalchemy_db_upgrade

如果我们添加,删除或更改任何列,我们总是可以执行migrateupgrade命令,以同样反映数据库中的这些更改。

创建和读取实体

在数据库就位并连接到我们的应用程序之后,剩下的就是实现CRUD操作了。 让我们从创建car开始,并检索所有当前存在的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Imports and CarsModel truncated

@app.route('/cars', methods=['POST', 'GET'])
def handle_cars():
    if request.method == 'POST':
        if request.is_json:
            data = request.get_json()
            new_car = CarsModel(name=data['name'], model=data['model'], doors=data['doors'])
            db.session.add(new_car)
            db.session.commit()
            return {"message": f"car {new_car.name} has been created successfully."}
        else:
            return {"error":"The request payload is not in JSON format"}

    elif request.method == 'GET':
        cars = CarsModel.query.all()
        results = [
            {
               "name": car.name,
               "model": car.model,
               "doors": car.doors
            } for car in cars]

        return {"count": len(results),"cars": results}

我们首先定义一个同时接受GETPOST请求的/cars路由。 GET请求将返回存储在我们数据库中的所有汽车的列表,而POST方法将以JSON格式接收汽车的数据,并使用提供的信息填充数据库。

要创建新车,我们使用CarsModel类,并提供填写cars表中各列所需的信息。 创建CarsModel对象后,我们创建一个数据库会话并将car添加到该会话。

为了将汽车保存到数据库中,我们通过db.session.commit()提交会话,这将关闭数据库事务并保存汽车。

让我们尝试使用Postman之类的工具添加汽车:

sqlalchemy_postman

响应消息通知我们我们的汽车已经创建并保存在数据库中:

sqlalchemy_db

您可以看到我们的数据库中现在有汽车的记录。

将汽车保存在我们的数据库中后,GET请求将帮助我们获取所有记录。 我们使用Flask-SQLAlchemy提供的CarsModel.query.all()函数查询数据库中存储的所有汽车。

这将返回CarsModel对象的列表,然后我们将其格式化并使用列表推导添加到列表中,并将其与数据库中的汽车数量一起传递给响应。 当我们通过Postman中的API请求提供汽车列表时:

sqlalchemy_postman_2

/cars端点上的GET方法返回出现在数据库中的汽车列表以及总数。

注意:请注意代码中没有单个SQL查询。 SQLAlchemy为我们解决了这一问题。

更新和删除实体

到目前为止,我们可以创建一辆汽车,并获取存储在数据库中的所有汽车的列表。 要在我们的API中完成对汽车的CRUD操作,我们需要添加功能以返回详细信息,修改和删除单个汽车。

我们将用来实现此目的的HTTP方法/动词是GETPUTDELETE,它们将通过一个称为handle_car()的方法组合在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Imports, Car Model, handle_cars() method all truncated

@app.route('/cars/<car_id>', methods=['GET', 'PUT', 'DELETE'])
def handle_car(car_id):
    car = CarsModel.query.get_or_404(car_id)

    if request.method == 'GET':
        response = {
           "name": car.name,
           "model": car.model,
           "doors": car.doors
        }
        return {"message":"success","car": response}

    elif request.method == 'PUT':
        data = request.get_json()
        car.name = data['name']
        car.model = data['model']
        car.doors = data['doors']
        db.session.add(car)
        db.session.commit()
        return {"message": f"car {car.name} successfully updated"}

    elif request.method == 'DELETE':
        db.session.delete(car)
        db.session.commit()
        return {"message": f"Car {car.name} successfully deleted."}

我们的方法handle_car()从URL接收car_id并获取存储在数据库中的汽车对象。 如果请求方法为GET,则将仅返回汽车详细信息:

sqlalchemy_postman_3

要更新我们的汽车的详细信息,我们使用PUT方法而不是PATCH。 两种方法都可以用来更新细节,但是PUT方法接受我们资源的更新版本,并替换我们已经存储在数据库中的版本。

PATCH方法只是修改了我们数据库中的那个而没有替换它。 因此,要更新数据库中的CarsModel记录,我们必须提供汽车的所有属性,包括要更新的属性。

我们使用这些细节来修改我们的汽车对象,并使用db.session.commit()提交这些更改,然后将响应返回给用户:

sqlalchemy_postman_4

我们的车已成功更新。

最后,要删除汽车,我们向同一端点发送DELETE请求。 在已经查询了CarsModel对象之后,我们需要做的就是使用当前会话通过执行db.session.delete(car)并提交事务以反映我们对数据库的更改来删除它:

sqlalchemy_postman_5

结论

现实生活中的应用程序不像我们的应用程序那么简单,它们通常处理相关的数据并分布在多个表中。

SQLAlchemy允许我们定义关系并处理相关数据。 有关处理关系的更多信息,请参见官方的Flask-SQLAlchemy文档。

我们的应用程序可以轻松扩展以容纳关系和更多表。 我们还可以使用Binds连接到多个数据库。 有关绑定的更多信息,请参见"绑定"文档页面。

在这篇文章中,我们介绍了ORM,尤其是SQLAlchemy ORM。 使用Flask和Flask-SQLAlchemy,我们创建了一个简单的API,用于公开和处理存储在本地PostgreSQL数据库中的汽车数据。

这篇文章中该项目的源代码可以在GitHub上找到。