如何使用 Flask 和 SQLite 实现一对多数据库关系

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

Flask是使用Python语言构建Web应用程序的框架,而 SQLite是可以与Python一起使用来存储应用程序数据的数据库引擎。

一个数据库关系是两个数据库表之间的关系,其中一个表中的记录可以引用另一个表中的多个记录。例如,在博客应用程序中,存储帖的表可以与存储评论的表具有一个对多个关系。每个帖子可以引用多个评论,每个评论引用单个帖子;因此,一个帖子有与多个评论的关系。

我们会使用SQLite,因为它是便携式的,不需要任何额外的设置来使用Python。它也非常适合在移动到更大的数据库(如MySQL或Postgres)之前进行应用程序的原型制作。 有关如何选择合适的数据库系统的更多信息,请阅读我们的文章。

前提条件

在你开始遵循这个指南之前,你将需要:

  • 一个本地的Python 3编程环境,请遵循本地计算机的 如何安装和设置Python 的本地编程环境 3系列的分布的教程。 在本教程中,我们将将我们的项目目录称为 flask_todo
  • 对创建路线、渲染HTML模板和连接到SQLite数据库等基本Flask概念的理解。

步骤一:创建数据库

在此步骤中,您将激活您的编程环境,安装Flask,创建SQLite数据库,并填充其样本数据。您将学习如何使用外部密钥来创建列表和项目之间的一个对多个关系。

如果您尚未激活您的编程环境,请确保您位于项目目录中(‘flask_todo’),并使用此命令来激活它:

1source env/bin/activate

一旦您的编程环境已激活,请使用以下命令安装Flask:

1pip install flask

一旦安装完成,您现在可以创建包含 SQL 命令的数据库架构文件,以创建您需要存储任务数据的表.您需要两个表:一个名为列表的表来存储任务列表,以及一个项目的表来存储每个列表的项目。

在您的flask_todo目录中打开名为schema.sql的文件:

1nano schema.sql

在此文件中输入以下 SQL 命令:

 1[label flask_todo/schema.sql]
 2DROP TABLE IF EXISTS lists;
 3DROP TABLE IF EXISTS items;
 4
 5CREATE TABLE lists (
 6    id INTEGER PRIMARY KEY AUTOINCREMENT,
 7    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
 8    title TEXT NOT NULL
 9);
10
11CREATE TABLE items (
12    id INTEGER PRIMARY KEY AUTOINCREMENT,
13    list_id INTEGER NOT NULL,
14    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
15    content TEXT NOT NULL,
16    FOREIGN KEY (list_id) REFERENCES lists (id)
17);

保存并关闭文件。

第一两个 SQL 命令是DROP TABLE IF EXISTS 列表;DROP TABLE IF EXISTS 项目;,这些删除任何已经存在的名为列表项目的表格,以便您不会看到令人困惑的行为。

接下来,您使用创建表列表创建列表表,该表将存储要做的事情列表(如学习列表、工作列表、主列表等)与下列列列:

  • id: 代表一个 primary key 的整数,这将被数据库分配给每个输入(即要做列表)的唯一值。
  • created: 要做列表创建的时刻. NOT NULL 意味着这个列不应该是空的,而 DEFAULT 值是 CURRENT_TIMESTAMP 值,即列表被添加到数据库的时刻。

然后,你创建了一个名为项目的表格来存储任务项目。这个表格有一个ID,一个list_id整数列来识别一个项目的列表属于哪个,创建日期和项目的内容。 要将项目链接到数据库中的一个列表,你使用一个外国密钥限制的字符串FOREIGN KEY(list_id) REFERENCES列表(id)。 这里的列表表是一个家长表,这是被外国密钥限制引用的表格,这表明一个列表可以有多个项目。

由于列表可以有 ** 多个** 项目,并且一个项目只属于 ** 一个** 列表,因此列表项目表之间的关系是一种 一对多 关系。

接下来,您将使用schema.sql文件创建数据库,在flask_todo目录中打开名为init_db.py的文件:

1nano init_db.py

然后添加以下代码:

 1[label flask_todo/init_db.py]
 2import sqlite3
 3
 4connection = sqlite3.connect('database.db')
 5
 6with open('schema.sql') as f:
 7    connection.executescript(f.read())
 8
 9cur = connection.cursor()
10
11cur.execute("INSERT INTO lists (title) VALUES (?)", ('Work',))
12cur.execute("INSERT INTO lists (title) VALUES (?)", ('Home',))
13cur.execute("INSERT INTO lists (title) VALUES (?)", ('Study',))
14
15cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
16            (1, 'Morning meeting')
17            )
18
19cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
20            (2, 'Buy fruit')
21            )
22
23cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
24            (2, 'Cook dinner')
25            )
26
27cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
28            (3, 'Learn Flask')
29            )
30
31cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
32            (3, 'Learn SQLite')
33            )
34
35connection.commit()
36connection.close()

保存并关闭文件。

在这里,您将连接到名为 database.db 的文件,该文件将在您执行该程序后创建,然后您将打开 schema.sql 文件,并使用 executescript() 方法运行它,同时执行多个 SQL 陈述。

运行schema.sql将创建列表项目表,然后使用指针对象(https://docs.python.org/3/library/sqlite3.html#cursor-objects),执行几个输入 SQL 陈述来创建三个列表和五个任务项目。

例如,工作列表是数据库中的第一个插入,所以它将具有1 ID。

最后,您执行更改并关闭连接。

运行这个程序:

1python init_db.py

執行後,一個名為「database.db」的新檔案會出現在您的「flask_todo」目錄中。

您已激活您的环境,安装了 Flask,并创建了 SQLite 数据库. 接下来,您将从数据库中获取列表和项目,并在应用程序的主页中显示它们。

步骤 2 – 显示要做的项目

在此步骤中,您将将您在上一步创建的数据库连接到显示每个列表的任务列表和项目的 Flask 应用程序,您将学习如何使用 SQLite 连接来查询来自两个表的数据,以及如何将任务列表组合到列表中。

首先,您将创建应用程序文件. 在flask_todo目录中打开名为app.py的文件:

1nano app.py

然后将以下代码添加到文件中:

 1[label flask_todo/app.py]
 2from itertools import groupby
 3import sqlite3
 4from flask import Flask, render_template, request, flash, redirect, url_for
 5
 6def get_db_connection():
 7    conn = sqlite3.connect('database.db')
 8    conn.row_factory = sqlite3.Row
 9    return conn
10
11app = Flask(__name__)
12app.config['SECRET_KEY'] = 'this should be a secret random string'
13
14@app.route('/')
15def index():
16    conn = get_db_connection()
17    todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
18                          ON i.list_id = l.id ORDER BY l.title;').fetchall()
19
20    lists = {}
21
22    for k, g in groupby(todos, key=lambda t: t['title']):
23        lists[k] = list(g)
24
25    conn.close()
26    return render_template('index.html', lists=lists)

保存并关闭文件。

「get_db_connection()」函数打开了连接到「database.db」数据库文件,然后将 row_factory属性设置为「sqlite3.Row」。这样,你可以以名称为基础访问列;这意味着数据库连接会返回行为像常规Python字典的行。

在 index() 视图中,打开数据库连接并执行以下 SQL 查询:

1SELECT i.content, l.title FROM items i JOIN lists l ON i.list_id = l.id ORDER BY l.title;

然后使用fetchall()方法获取结果,并将数据保存到名为todos的变量中。

在此查询中,您使用SELECT来获取项目的内容和它所属的列表的标题,通过连接项目列表表(以项目i列表l的表副名称)。在ON关键字之后加入条件i.list_id = l.id,您将从列表表中获取项目的每个行,并从列表表中获取列表的每个行,而项目表的列表_id列表与列表表的id匹配。

要更好地理解此查询,请在您的 flask_todo 目录中打开 Python REPL:

1python

要了解 SQL 查询,请通过运行此小程序来检查todos变量的内容:

1from app import get_db_connection
2conn = get_db_connection()
3todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
4ON i.list_id = l.id ORDER BY l.title;').fetchall()
5for todo in todos:
6    print(todo['title'], ':', todo['content'])

你首先从app.py文件中导入get_db_connection,然后打开一个连接并执行查询(请注意,这是你在app.py文件中所使用的相同的SQL查询)。

产量将如下:

1[secondary_label Output]
2Home : Buy fruit
3Home : Cook dinner
4Study : Learn Flask
5Study : Learn SQLite
6Work : Morning meeting

使用CTRL + D关闭 REPL。

现在你已经明白了 SQL 如何合并工作以及查询实现了什么,让我们回到你的 app.py 文件中的 index() 视图函数。

1lists = {}
2
3for k, g in groupby(todos, key=lambda t: t['title']):
4    lists[k] = list(g)

您首先宣布一个名为列表的空字典,然后使用for循环来通过列表的标题对todos变量中的结果进行组合。您使用从itertools标准库中导入的groupby()(https://docs.python.org/3.5/library/itertools.html#itertools.groupby)函数。

k代表列表标题(即, Home, Study, Work),这些标题是使用您转移到 groupby() 函数的 key 参数的函数提取的。在这种情况下,该函数是 lambda t: t['title'],它采取一个任务项,并返回列表的标题(就像您之前在前一个循环中使用过 todo['title'] 的函数一样)。 g代表包含每个列表标题的任务项的组。例如,在第一个迭代中, k将是 Home,而 g则是一个 [ITERABLE](LINK)将包含 Buy fruitCook dinner' 的项目。

这为我们提供了列表和项目之间一对多关系的代表性,每个列表标题都有多个要做项目。

執行「app.py」檔案時,以及「for」循環完成執行後,「列表」將如下:

1[secondary_label Output]
2{'Home': [<sqlite3.Row object at 0x7f9f58460950>,
3          <sqlite3.Row object at 0x7f9f58460c30>],
4 'Study': [<sqlite3.Row object at 0x7f9f58460b70>,
5           <sqlite3.Row object at 0x7f9f58460b50>],
6 'Work': [<sqlite3.Row object at 0x7f9f58460890>]}

每个sqlite3.Row对象将包含您使用index()函数中的 SQL 查询从项目表中获取的数据。

在您的flask_todo目录中打开名为list_example.py的文件:

1nano list_example.py

然后添加以下代码:

 1[label flask_todo/list_example.py]
 2
 3from itertools import groupby
 4from app import get_db_connection
 5
 6conn = get_db_connection()
 7todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
 8                        ON i.list_id = l.id ORDER BY l.title;').fetchall()
 9
10lists = {}
11
12for k, g in groupby(todos, key=lambda t: t['title']):
13    lists[k] = list(g)
14
15for list_, items in lists.items():
16    print(list_)
17    for item in items:
18        print('    ', item['content'])

保存并关闭文件。

这与您的index()视图函数中的内容非常相似. 这里的最后一个for循环说明了lists字典的结构。 您首先通过字典的项目,打印列表标题(位于list_变量中),然后通过列表中的每个任务项目组,打印该项目的内容值。

运行list_example.py程序:

1python list_example.py

以下是list_example.py的输出:

1[secondary_label Output]
2Home
3     Buy fruit
4     Cook dinner
5Study
6     Learn Flask
7     Learn SQLite
8Work
9     Morning meeting

现在你已经理解了index()函数的每个部分,让我们创建一个基础模板,并使用return render_template('index.html', lists=lists)字段创建你渲染的index.html文件。

在您的flask_todo目录中,创建一个模板目录,并在其内部打开一个名为base.html的文件:

1mkdir templates
2nano templates/base.html

如果您不熟悉 Flask 中的 HTML 模板,请参阅 How To Make A Web Application Using Flask in Python 3:

 1[label flask_todo/templates/base.html]
 2<!doctype html>
 3<html lang="en">
 4  <head>
 5    <!-- Required meta tags -->
 6    <meta charset="utf-8">
 7    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 8
 9    <!-- Bootstrap CSS -->
10    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
11
12    <title>{% block title %} {% endblock %}</title>
13  </head>
14  <body>
15    <nav class="navbar navbar-expand-md navbar-light bg-light">
16        <a class="navbar-brand" href="{{ url_for('index')}}">FlaskTodo</a>
17        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
18            <span class="navbar-toggler-icon"></span>
19        </button>
20        <div class="collapse navbar-collapse" id="navbarNav">
21            <ul class="navbar-nav">
22            <li class="nav-item active">
23                <a class="nav-link" href="#">About</a>
24            </li>
25            </ul>
26        </div>
27    </nav>
28    <div class="container">
29        {% block content %} {% endblock %}
30    </div>
31
32    <!-- Optional JavaScript -->
33    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
34    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
35    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
36    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
37  </body>
38</html>

保存并关闭文件。

上一块的大部分代码是标准的HTML和对Bootstrap所需的代码。<meta>标签为Web浏览器提供信息,<link>标签链接到Bootstrap CSS文件,而<script>标签是链接到JavaScript代码,允许一些额外的Bootstrap功能。

接下来,创建将这个base.html文件扩展的index.html文件:

1nano templates/index.html

将以下代码添加到 index.html 中:

 1[label flask_todo/templates/index.html]
 2{% extends 'base.html' %}
 3
 4{% block content %}
 5    <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1>
 6    {% for list, items in lists.items() %}
 7        <div class="card" style="width: 18rem; margin-bottom: 50px;">
 8            <div class="card-header">
 9                <h3>{{ list }}</h3>
10            </div>
11            <ul class="list-group list-group-flush">
12                {% for item in items %}
13                    <li class="list-group-item">{{ item['content'] }}</li>
14                {% endfor %}
15            </ul>
16        </div>
17    {% endfor %}
18{% endblock %}

在这里,您使用循环来浏览列表字典的每个项目,将列表标题显示为h3标签中的卡头,然后使用列表组在h3标签中显示属于列表的每个任务项目。

您现在将设置环境变量 Flask 需求,并使用以下命令运行应用程序:

1export FLASK_APP=app
2export FLASK_ENV=development
3flask run

一旦开发服务器运行,您可以在浏览器中访问URL http://127.0.0.1:5000/,您将看到一个网页,上面有欢迎来到FlaskTodo和您的列表项目。

Home Page

您现在可以键入CTRL + C来停止您的开发服务器。

您已经创建了一个Flask应用程序,显示每个列表的任务列表和项目. 在下一步,您将添加一个新页面来创建新的任务项目。

步骤 3 – 添加新任务

在此步骤中,您将创建一个新的路径来创建任务项目,将数据插入到数据库表中,并将项目与它们所属的列表相关联。

首先,打开 app.py 文件:

1nano app.py

然后,在文件末尾添加一个新的/create路径,其中有一个名为create()的视图函数:

1[label flask_todo/app.py]
2...
3@app.route('/create/', methods=('GET', 'POST'))
4def create():
5    conn = get_db_connection()
6    lists = conn.execute('SELECT title FROM lists;').fetchall()
7
8    conn.close()
9    return render_template('create.html', lists=lists)

保存并关闭文件。

由于您将使用此路径通过 Web 表单将新数据插入到数据库中,因此您可以使用app.route()装饰器中的methods=('GET', 'POST')来允许 GET 和 POST 请求。

接下来,打开一个名为create.html的新模板文件:

1nano templates/create.html

将以下 HTML 代码添加到 create.html:

 1[label flask_todo/templates/create.html]
 2{% extends 'base.html' %}
 3
 4{% block content %}
 5<h1>{% block title %} Create a New Item {% endblock %}</h1>
 6
 7<form method="post">
 8    <div class="form-group">
 9        <label for="content">Content</label>
10        <input type="text" name="content"
11               placeholder="Todo content" class="form-control"
12               value="{{ request.form['content'] }}"></input>
13    </div>
14
15    <div class="form-group">
16        <label for="list">List</label>
17        <select class="form-control" name="list">
18            {% for list in lists %}
19                {% if list['title'] == request.form['list'] %}
20                    <option value="{{ request.form['list'] }}" selected>
21                        {{ request.form['list'] }}
22                    </option>
23                {% else %}
24                    <option value="{{ list['title'] }}">
25                        {{ list['title'] }}
26                    </option>
27                {% endif %}
28            {% endfor %}
29        </select>
30    </div>
31    <div class="form-group">
32        <button type="submit" class="btn btn-primary">Submit</button>
33    </div>
34</form>
35{% endblock %}

保存并关闭文件。

您使用request.form来访问存储的表单数据,如果您的表单提交错误(例如,如果没有提供任务内容)。在<select>元素中,您在Create()函数中循环从数据库中获取的列表。

现在,在终端中,运行您的 Flask 应用程序:

1flask run

然后,在您的浏览器中访问http://127.0.0.1:5000/create,您将看到创建一个新的任务项目的表格,请注意,该表格尚未工作,因为您没有代码来处理浏览器发送的POST请求。

键入CTRL + C来停止您的开发服务器。

接下来,让我们将处理 POST 请求的代码添加到 create() 函数中,并使表单正常工作,打开 app.py:

1nano app.py

然后编辑创建()函数看起来像这样:

 1[label flask_todo/app.py]
 2...
 3@app.route('/create/', methods=('GET', 'POST'))
 4def create():
 5    conn = get_db_connection()
 6
 7    if request.method == 'POST':
 8        content = request.form['content']
 9        list_title = request.form['list']
10
11        if not content:
12            flash('Content is required!')
13            return redirect(url_for('index'))
14
15        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
16                                 (list_title,)).fetchone()['id']
17        conn.execute('INSERT INTO items (content, list_id) VALUES (?, ?)',
18                     (content, list_id))
19        conn.commit()
20        conn.close()
21        return redirect(url_for('index'))
22
23    lists = conn.execute('SELECT title FROM lists;').fetchall()
24
25    conn.close()
26    return render_template('create.html', lists=lists)

保存并关闭文件。

request.method ==POST条件中,您可以从表单数据中获取要做项目的内容和列表的标题。如果没有提交内容,您会使用flash()函数向用户发送消息,并重定向到索引页面。如果这个条件没有触发,那么您会执行一个SELECT声明,从提供的列表标题中获取列表ID,并将其保存到名为list_id`的变量中。

作为最后一步,您将在导航栏中添加一个链接到/create,并在其下方显示闪存消息,要做到这一点,打开base.html:

1nano templates/base.html

通过添加一个新的<li>导航项目来编辑该文件,链接到create()视图函数,然后使用for循环在content块上方显示闪存的消息,这些信息可以在get_flashed_messages()闪存函数中显示(https://flask.palletsprojects.com/en/1.1.x/patterns/flashing/):

 1[label flask_todo/templates/base.html]
 2<nav class="navbar navbar-expand-md navbar-light bg-light">
 3    <a class="navbar-brand" href="{{ url_for('index')}}">FlaskTodo</a>
 4    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
 5        <span class="navbar-toggler-icon"></span>
 6    </button>
 7    <div class="collapse navbar-collapse" id="navbarNav">
 8        <ul class="navbar-nav">
 9        <li class="nav-item active">
10            <a class="nav-link" href="{{ url_for('create') }}">New</a>
11        </li>
12
13        <li class="nav-item active">
14            <a class="nav-link" href="#">About</a>
15        </li>
16        </ul>
17    </div>
18</nav>
19<div class="container">
20    {% for message in get_flashed_messages() %}
21        <div class="alert alert-danger">{{ message }}</div>
22    {% endfor %}
23    {% block content %} {% endblock %}
24</div>

保存并关闭文件。

现在,在终端中,运行您的 Flask 应用程序:

1flask run

如果您导航到此页面并尝试添加一个没有内容的新任务项目,您将收到一个闪烁的消息说 内容是必需的!. 如果您填写内容表单,一个新的任务项目将出现在索引页面上。

在此步骤中,您已添加创建新任务项目并将其保存到数据库的功能。

您可以找到此项目的源代码在 此存储库

结论

您现在有一个应用程序来管理要做列表和项目. 每个列表都有多个要做项目,每个要做项目属于一个单一的列表在一对多关系中。 您了解了如何使用Flask和SQLite来管理多个相关的数据库表,如何使用 _foreign keys,以及如何使用SQLite joins在Web应用程序中从两个表中获取和显示相关数据。

此外,您使用「groupby()」函数组合结果,将新数据插入到数据库中,并将相关的数据库表行与它们相关的表组合。

您还可以阅读更多我们的 Python Framework 内容. 如果您想查看 Python 模块,请阅读我们的教程在 如何在 Python 中使用 sqlite3 模块 3上。

Published At
Categories with 技术
comments powered by Disqus