Featured image of post Python Web开发学习(二):使用PostgreSQL和SQLAlchemy

Python Web开发学习(二):使用PostgreSQL和SQLAlchemy

在Flask框架内使用PostgreSQL数据库和SQLAlchemy操作数据库

缘起

在本系列的上一篇文章“Python Web开发学习(一):使用Flask框架”中,我们使用Flask框架开发了一个简单的Web应用程序,来显示一些城市的天气信息。但是这些天气信息都是写死在代码中的,如果我们有许多的天气信息,再写死在代码中就不太方便了。因此我们需要一个数据库来存储这些天气信息,然后从数据库中读取这些天气信息,再显示在网页上。

本文将介绍如何使用PostgreSQL数据库和SQLAlchemy操作数据库,来实现从数据库中读取天气信息。

前置条件

在开始本文之前,确保你已经清楚了如何用Flask框架开发一个简单的Web应用程序,如果不清楚,可以参考本系列的上一篇文章“Python Web开发学习(一):使用Flask框架”

PostgreSQL

PostgreSQL简介

PostgreSQL是一个开源的关系型数据库管理系统,它的特点是功能强大、可扩展性好、支持SQL标准、支持事务、支持复杂查询、支持多种编程语言、支持多种操作系统。PostgreSQL是最流行的关系型数据库管理系统之一,因此这里我们选择使用PostgreSQL数据库来作为Web应用后端的数据库。

安装配置PostgreSQL

  1. 在Ubuntu 22中安装PostgreSQL非常简单,只需要在终端中运行以下命令即可:

    1
    
    sudo apt install postgresql
    
  2. 使用下面的命令可以查看安装的PostgreSQL的版本:

    1
    
    psql --version
    
  3. 安装好之后,PostgreSQL会自动创建一个名为postgres的用户,这个用户是超级用户,可以用来管理数据库。我们可以使用下面的命令来切换到postgres用户:

    1
    
    sudo su postgres
    

    登录后我们以postgres用户的身份登录到系统的命令后中。然后运行下面的命令来登录到PostgreSQL数据库:

    1
    
    psql
    

    下面是PostgreSQL中常用的命令:

    • \l:列出所有的数据库
    • \?:列出所有的命令
    • \c <database_name>:连接到指定的数据库
    • \d:列出当前数据库中的所有表
    • \d <table_name>:列出指定表的结构
    • \q:退出PostgreSQL
    • \du:列出所有的用户
    • \du+:列出所有的用户和用户的详细信息
    • \password <user_name>:修改指定用户的密码
    • \conninfo:显示当前连接的数据库信息
  4. 使用\du列出所有用户,可以发现目前只有一个用户postgres。我们需要创建一个新的用户,用来管理我们的数据库。使用下面的命令创建一个新的用户:

    1
    
    CREATE USER test WITH PASSWORD 'test_password';
    

    这里的test是用户名,test是密码。然后使用下面的命令给这个用户授予超级用户权限:

    1
    
    ALTER USER test WITH SUPERUSER;
    

    然后使用下面的命令退出PostgreSQL:

    1
    
    \q
    

    然后使用下面的命令退出postgres用户:

    1
    
    exit
    
  5. 然后我们重新用新建的用户test登录到PostgreSQL数据库:

    1
    
    psql -U test
    

    这时可能会遇到下面的报错信息:

    1
    
    FATAL:  Peer authentication failed for user "test"
    

    解决这个问题需要修改一下PostgreSQL的配置文件(参考https://zhuanlan.zhihu.com/p/467644334),使用下面的命令打开PostgreSQL的配置文件:

    1
    
    sudo vim /etc/postgresql/14/main/pg_hba.conf
    

    其中14是PostgreSQL的版本号,如果你的版本号不是14,请修改为你的版本号。在配置文件的最后可以看到这样的几行信息:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    # Database administrative login by Unix domain socket
    local   all             postgres                                peer
    
    # TYPE  DATABASE        USER            ADDRESS                 METHOD
    
    # "local" is for Unix domain socket connections only
    local   all             all                                     peer
    # IPv4 local connections:
    host    all             all             127.0.0.1/32            scram-sha-256
    # IPv6 local connections:
    host    all             all             ::1/128                 scram-sha-256
    # Allow replication connections from localhost, by a user with the
    # replication privilege.
    local   replication     all                                     peer
    host    replication     all             127.0.0.1/32            scram-sha-256
    host    replication     all             ::1/128                 scram-sha-256
    

    我们需要把local all all peer这一行的peer改为md5,然后保存退出。最后使用下面的命令重启PostgreSQL:

    1
    
    sudo service postgresql restart
    

    这时再重新尝试登录到PostgreSQL,就不会报错了:

    1
    
    psql -U test
    
  6. 最后为我们的Web应用创建一个新的数据库:

    1
    
    CREATE DATABASE weather_db;
    

    这里的weather_db是数据库的名称。然后使用下面的命令退出PostgreSQL:

    1
    
    \q
    

在Flask中使用PostgreSQL

安装psycopg2和SQLAlchemy

psycopg2是一个用于连接PostgreSQL数据库的Python库,SQLAlchemy是一个用于操作数据库的Python库。我们需要安装这两个库。先确保我们已经激活了Python虚拟环境,然后使用下面的命令:

1
2
pip install psycopg2-binary
pip install SQLAlchemy

创建数据库模型

  1. 在Flask应用中连接PostgreSQL数据库,使用下面的代码:

    1
    2
    3
    4
    5
    6
    7
    
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://test:test_password@localhost:5432/weather_db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db = SQLAlchemy(app)
    

    上面的代码中,我们首先导入Flask类和SQLAlchemy类,然后创建一个Flask应用程序,然后使用app.config来配置数据库的连接信息,其中SQLALCHEMY_DATABASE_URI是数据库的连接地址,SQLALCHEMY_TRACK_MODIFICATIONS是用来关闭对模型修改的监控,因为这个功能会消耗额外的内存,但是我们并不需要这个功能。最后我们创建一个db对象,用来操作数据库。

    注意:这里的数据库使用端口号5432,这是PostgreSQL的默认端口号,如果你的PostgreSQL使用的端口号不是5432,请修改为你的端口号。在PostgreSQL的配置文件中可以查看PostgreSQL使用的端口号。配置文件的路径是/etc/postgresql/14/main/postgresql.conf,其中14是PostgreSQL的版本号,如果你的版本号不是14,请修改为你的版本号。

  2. 然后需要创建数据库模型,以在Flask中使用SQLAlchemy操作数据库。我们先导入SQLAlchemy库,然后创建一个db对象,用来操作数据库。然后我们需要定义一个Weather类,用来表示城市的天气信息。Weather类继承自db.Model类,这样Weather类就可以使用db对象来操作数据库。Weather类有数个属性,和我们之前用字典定义的差不多。最后我们需要使用db.create_all()方法来创建数据库表,这个方法会根据我们定义的数据库模型来创建数据库表。代码如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    db = SQLAlchemy(app)
    
    class Weather(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(50), nullable=False)
        country = db.Column(db.String(50), nullable=False)
        temp = db.Column(db.Integer, nullable=False)
        feels_like = db.Column(db.Integer, nullable=False)
        icon = db.Column(db.String(50), nullable=False)
        description = db.Column(db.String(50), nullable=False)
    
    db.create_all()
    

在应用中使用数据库

  1. 之前我们用字典来保存天气信息,现在我们改为用数据库来保存。代码如下:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    db.session.add(Weather(
        name = 'London',
        country = 'UK',
        temp = '12',
        feels_like = '11',
        icon = '10d',
        description = 'Moderate rain'
    ))
    db.session.add(Weather(
        name = 'New York',
        country = 'US',
        temp = '20',
        feels_like = '19',
        icon = '01d',
        description = 'Sunny'
    ))
    db.session.commit()
    
  2. 然后我们需要从数据库中读取天气信息,然后传递给模板。注意,之前的HTML模板接收一个字典数组作为参数,现在我们需要传递一个Weather对象的数组,因此我们需要修改一下模板:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Weather App</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
    </head>
    <body>
        <section class="ajax-section">
            <div class="container">
                <ul class="cities">
                    {% for wd in weather_data %}
                    <li class="city">
                        <h2 class="city-name">{{ wd.name }}, <sup>{{ wd.country }}</sup></h2>
                        <div class="city-temp">{{ wd.temp }}<sup>°C</sup></div>
                        <figcaption>{{ wd.description }}</figcaption>
                        <img class="city-icon" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/162656/{{ wd.icon }}.svg" alt="{{ wd.description }}">
                    </li>
                    {% endfor %}
                </ul>
            </div>
        </section>
    </body>
    </html>
    
    Python中获取数据库数据并传递给模板的代码如下:
    1
    2
    
    cities_data = Weather.query.all()
    return render_template('weather.html', cities_data=cities_data)
    
    这里的cities_data是一个列表,列表中的元素是一个Weather对象,我们可以在模板中使用这个对象的属性来获取天气信息。

运行Web应用程序

用下面的命令尝试运行我们的Web应用程序:

1
python app.py

这时可能会遇到下面的报错信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Traceback (most recent call last):
  File "/home/lijin/Documents/www/learn_flask/app.py", line 21, in <module>
    db.create_all()
  File "/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py", line 900, in create_all
    self._call_for_binds(bind_key, "create_all")
  File "/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py", line 871, in _call_for_binds
    engine = self.engines[key]
             ^^^^^^^^^^^^
  File "/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py", line 687, in engines
    app = current_app._get_current_object()  # type: ignore[attr-defined]
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/werkzeug/local.py", line 508, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.

这是因为我们的应用程序需要一个应用上下文,我们需要在app.py中添加下面的代码:

1
2
with app.app_context():
    db.create_all()

最终的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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://test:test_password@localhost:5432/weather_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class Weather(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    country = db.Column(db.String(50), nullable=False)
    temp = db.Column(db.Integer, nullable=False)
    feels_like = db.Column(db.Integer, nullable=False)
    icon = db.Column(db.String(50), nullable=False)
    description = db.Column(db.String(50), nullable=False)

@app.route('/')
def index():
    db.session.add(Weather(
        name = 'London',
        country = 'UK',
        temp = '12',
        feels_like = '11',
        icon = '10d',
        description = 'Moderate rain'
    ))
    db.session.add(Weather(
        name = 'New York',
        country = 'US',
        temp = '20',
        feels_like = '19',
        icon = '01d',
        description = 'Sunny'
    ))
    db.session.commit()
    wd = Weather.query.all()
    print(len(wd))

    return render_template('weather.html', weather_data=wd)

if __name__ == "__main__":
    with app.app_context():
        db.create_all()
        app.run()

然后我们再次运行应用程序:

1
python app.py

不出意外的话,我们可以在浏览器中看到我们的网站在两个卡片里展示了两个城市的天气信息,和上一篇文章中的效果是一样的: weather app

注意:每次运行应用程序时,都会向数据库中添加两条数据,因此如果多次运行应用程序,可能会导致数据库中有多条重复的数据,网页上也会出现多个重复的卡片。这时我们可以使用下面的代码来删除重复的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from sqlalchemy import text

sql_text = text(
     """DELETE FROM weather
    WHERE id IN (
        SELECT id
        FROM (
            SELECT
                id,
                ROW_NUMBER() OVER (PARTITION BY name) AS row_num
            FROM
                weather
        ) AS numbered_rows
        WHERE row_num > 1
    );
    """)
db.session.execute(sql_text)
db.session.commit()

总结

至此,我们成功创建了一个PostgreSQL数据库,然后在Flask应用程序中使用SQLAlchemy操作数据库。但是数据库中的数据仍然是我们一条一条手动添加的,这样是很不灵活也很不方便的。下一篇文章我们将介绍如何获取用户的输入,然后根据用户输入的城市使用API来获取天气信息,然后把获取到的天气信息保存到数据库中。

comments powered by Disqus