作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
Flask 是一个使用 Python 语言构建 Web 应用程序的框架,而 SQLite 是一个数据库引擎,可以与 Python 一起使用来存储应用程序数据。 在本教程中,您将通过添加多对多关系来修改使用 Flask 和 SQLite 构建的应用程序。
尽管您可以独立学习本教程,但它也是 如何使用 Flask 和 SQLite 修改一对多数据库关系中的项目 教程的延续,在该教程中,我们使用使用待办事项应用示例的一对多关系。 该应用程序允许用户添加新的待办事项、将项目分类到不同的列表下以及修改项目。
多对多 数据库关系是两个表之间的关系,其中每个表中的一条记录可以引用另一个表中的多条记录。 例如,在博客中,用于帖子的表可以与用于存储作者的表具有多对多关系。 每个帖子可以引用多个作者,每个作者可以引用多个帖子。 每个帖子可以有many个作者,每个作者可以写many个帖子。 因此,帖子和作者之间存在多对多的关系。 再举一个例子,在社交媒体应用程序中,每个帖子可能有很多标签,每个标签可能有很多帖子。
在本教程结束时,您的应用程序将拥有一项新功能,用于将待办事项分配给不同的用户。 我们将使用单词 assignees 来指代被分配待办事项的用户。 例如,您可以为 Cleaning the kitchen
有一个家庭待办事项,您可以将其分配给 Sammy
和 Jo
— 每个待办事项可以有 多个 受让人(即 Sammy
和 Jo
)。 另外每个用户可以有many个to-dos分配给他们(也就是说,Sammy
可以分配多个to-do item),这是to-do之间的多对多关系做项目和受让人。
在本教程结束时,应用程序将包含一个 Assigned to 标记,其中列出了受让人的姓名。
先决条件
在开始遵循本指南之前,您需要:
- 本地 Python 3 编程环境,请按照 如何为 Python 3 系列安装和设置本地编程环境中的分发教程进行操作。 在本教程中,我们将调用我们的项目目录
flask_todo
。 - (可选)在 Step 1 中,您可以选择克隆您将在本教程中使用的待办事项应用程序。 但是,您可以选择通过 如何使用 Flask 和 SQLite 的一对多数据库关系 和 如何使用 Flask 和 SQLite 修改一对多数据库关系中的项目[X207X ]。 您可以从此页面访问最终代码。
- (可选)了解基本的 Flask 概念,例如创建路由、呈现 HTML 模板和连接到 SQLite 数据库。 如果您不熟悉这些概念,请查看 如何在 Python 3 中使用 Flask 和 如何在 Python 3 中使用 sqlite3 模块 制作 Web 应用程序,但这不是必需的。
第 1 步 — 设置 Web 应用程序
在此步骤中,您将设置待办事项应用程序以进行修改。 您还将查看数据库模式以了解数据库的结构。 如果您按照先决条件部分中的教程进行操作,并且本地计算机上仍有代码和虚拟环境,则可以跳过此步骤。
为了演示向 Flask Web 应用程序添加多对多关系,您将使用上一教程的应用程序代码,这是一个使用 Flask、SQLite[X215X 构建的待办事项管理 Web 应用程序] 和 Bootstrap 框架 。 使用此应用程序,用户可以创建新的待办事项,修改和删除现有的待办事项,并将待办事项标记为完成。
使用以下命令克隆存储库并将其从 flask-todo-2
重命名为 flask_todo
:
git clone https://github.com/do-community/flask-todo-2 flask_todo
导航到 flask_todo
:
cd flask_todo
然后创建一个新的虚拟环境:
python -m venv env
激活环境:
source env/bin/activate
安装烧瓶:
pip install Flask
然后,使用 init_db.py
程序初始化数据库:
python init_db.py
接下来,设置以下环境变量:
export FLASK_APP=app export FLASK_ENV=development
FLASK_APP
表示您当前正在开发的应用程序,本例为app.py
。 FLASK_ENV
指定模式—设置为 development
为开发模式; 这将允许您调试应用程序。 (切记不要在生产环境中使用此模式。)
然后运行开发服务器:
flask run
如果您使用浏览器,您将在以下 URL 上运行应用程序:http://127.0.0.1:5000/
。
要停止开发服务器,请使用 CTRL + C
。
接下来,您将通过数据库模式来了解表之间的当前关系。 如果您熟悉schema.sql
文件的内容,可以跳到下一步。
打开schema.sql
文件:
nano schema.sql
文件内容如下:
flask_todo/schema.sql
DROP TABLE IF EXISTS lists; DROP TABLE IF EXISTS items; CREATE TABLE lists ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL ); CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, list_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (list_id) REFERENCES lists (id) );
在 schema.sql
文件中,有两个表:lists
用于存储列表(例如 Home
或 Study
),items
用于存储列表存储待办事项(例如 Do the dishes
或 Learn Flask
)。
lists
表具有以下列:
id
:列表的ID。created
:列表的创建日期。title
:列表的标题。
items
表具有以下列:
id
:物品的ID。list_id
:项目所属列表的ID。created
:项目的创建日期。content
:项目的内容。done
:项目状态,0
表示项目尚未完成,1
表示项目完成。
在 items
表中,您有一个 外键约束 ,其中 list_id
列引用 lists
父级的 id
列桌子。 这是item和list的一对多关系,表示一个list可以有多个item,item属于一个list:
FOREIGN KEY (list_id) REFERENCES lists (id)
在下一步中,您将使用多对多关系在两个表之间创建链接。
第 2 步 — 添加受让人表
在此步骤中,您将了解如何实现多对多关系和连接表。 然后,您将添加一个用于存储受让人的新表。
多对多关系链接两个表,其中一个表中的每个项目在另一个表中都有许多相关项目。
假设您有一个简单的待办事项表,如下所示:
Items +----+-------------------+ | id | content | +----+-------------------+ | 1 | Buy eggs | | 2 | Fix lighting | | 3 | Paint the bedroom | +----+-------------------+
还有一个受让人的表格,如下所示:
assignees +----+------+ | id | name | +----+------+ | 1 | Sammy| | 2 | Jo | +----+------+
假设您要将待办事项 Fix lighting
分配给 Sammy
和 Jo
,您可以通过在 items
表中添加新行来实现像这样:
items +----+-------------------+-----------+ | id | content | assignees | +----+-------------------+-----------+ | 1 | Buy eggs | | | 2 | Fix lighting | 1, 2 | | 3 | Paint the bedroom | | +----+-------------------+-----------+
这是错误的方法,因为每一列应该只有一个值; 如果您有多个值,则添加和更新数据等基本操作会变得繁琐且缓慢。 相反,应该有第三个表引用相关表的主键——这个表通常称为 连接表 ,它存储每个表中每个项目的 ID。
以下是链接项目和受让人之间的连接表的示例:
item_assignees +----+---------+-------------+ | id | item_id | assignee_id | +----+---------+-------------+ | 1 | 2 | 1 | | 2 | 2 | 2 | +----+---------+-------------+
在第一行中,ID 为 2
(即 Fix lighting
)的项目与 ID 为 1
(Sammy
)的受让人相关。 在第二行中,同一项目还与 ID 为 2
(Jo
) 的受让人相关。 这意味着待办事项被分配给 Sammy
和 Jo
。 同样,您可以将每个受理人分配给多个项目。
现在,您将修改待办事项应用程序的数据库以添加一个用于存储受让人的表。
首先,打开 schema.sql
添加一个名为 assignees
的新表:
nano schema.sql
如果 assignees
表已存在,则添加一行以删除它。 这是为了避免在重新启动数据库时潜在的未来问题,例如已经存在的具有不同列的 assignees
表,如果它不遵循相同的架构,这可能会意外地破坏代码。 您还为表添加了 SQL 代码:
flask_todo/schema.sql
DROP TABLE IF EXISTS assignees; DROP TABLE IF EXISTS lists; DROP TABLE IF EXISTS items; CREATE TABLE lists ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL ); CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, list_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (list_id) REFERENCES lists (id) ); CREATE TABLE assignees ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL );
保存并关闭文件。
这个新的 assignees
表具有以下列:
id
:受让人的ID。name
:受让人的名称。
编辑 init_db.py
程序,将一些受理人添加到数据库中。 你使用这个程序来初始化数据库:
nano init_db.py
修改文件如下所示:
flask_todo/init_db.py
import sqlite3 connection = sqlite3.connect('database.db') with open('schema.sql') as f: connection.executescript(f.read()) cur = connection.cursor() cur.execute("INSERT INTO lists (title) VALUES (?)", ('Work',)) cur.execute("INSERT INTO lists (title) VALUES (?)", ('Home',)) cur.execute("INSERT INTO lists (title) VALUES (?)", ('Study',)) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (1, 'Morning meeting') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (2, 'Buy fruit') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (2, 'Cook dinner') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (3, 'Learn Flask') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (3, 'Learn SQLite') ) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Sammy',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Jo',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Charlie',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Ashley',)) connection.commit() connection.close()
保存并关闭文件。
在突出显示的行中,您使用 游标对象 执行 INSERT
SQL 语句以将四个名称插入到 assignees
表中。 您在 execute() 方法中使用 ?
占位符并传递一个包含受让人名称的元组以安全地将数据插入数据库。 然后使用 connection.commit()
提交事务并使用 connection.close()
关闭连接。
这将向数据库添加四个受让人,名称为 Sammy
、Jo
、Charlie
和 Ashley
。
运行 init_db.py
程序重新初始化数据库:
python init_db.py
您现在有一个用于在数据库中存储受让人的表。 接下来,您将添加一个连接表以在项目和受理人之间创建多对多关系。
第 3 步 — 添加多对多连接表
在此步骤中,您将使用连接表将待办事项与受让人链接。 首先,您将编辑数据库模式文件以添加新的连接表,编辑数据库初始化程序以添加一些分配,然后使用演示程序显示每个待办事项的分配者。
打开 schema.sql
添加新表:
nano schema.sql
因为该表连接了项目和受让人,所以您将其称为 item_assignees
。 如果表已经存在,则添加一行删除表,然后为表本身添加 SQL 代码:
flask_todo/schema.sql
DROP TABLE IF EXISTS assignees; DROP TABLE IF EXISTS lists; DROP TABLE IF EXISTS items; DROP TABLE IF EXISTS item_assignees; CREATE TABLE lists ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL ); CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, list_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (list_id) REFERENCES lists (id) ); CREATE TABLE assignees ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ); CREATE TABLE item_assignees ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER, assignee_id INTEGER, FOREIGN KEY(item_id) REFERENCES items(id), FOREIGN KEY(assignee_id) REFERENCES assignees(id) );
保存并关闭文件。
这个新的 item_assignees
表具有以下列:
id
:建立待办事项和受理人关系的条目ID; 每行代表一个关系。item_id
:将分配给对应assignee_id
的受理人的待办事项ID。assignee_id
:将被分配具有相应item_id
的项目的受让人的 ID。
item_assignees
表也有两个外键约束:一个链接 item_id
列与 items
表的 id
列,另一个链接assignee_id
列与 assignees
表的 id
列。
打开init_db.py
添加几个赋值:
nano init_db.py
修改文件如下所示:
flask_todo/init_db.py
import sqlite3 connection = sqlite3.connect('database.db') with open('schema.sql') as f: connection.executescript(f.read()) cur = connection.cursor() cur.execute("INSERT INTO lists (title) VALUES (?)", ('Work',)) cur.execute("INSERT INTO lists (title) VALUES (?)", ('Home',)) cur.execute("INSERT INTO lists (title) VALUES (?)", ('Study',)) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (1, 'Morning meeting') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (2, 'Buy fruit') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (2, 'Cook dinner') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (3, 'Learn Flask') ) cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)", (3, 'Learn SQLite') ) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Sammy',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Jo',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Charlie',)) cur.execute("INSERT INTO assignees (name) VALUES (?)", ('Ashley',)) # Assign "Morning meeting" to "Sammy" cur.execute("INSERT INTO item_assignees (item_id, assignee_id) VALUES (?, ?)", (1, 1)) # Assign "Morning meeting" to "Jo" cur.execute("INSERT INTO item_assignees (item_id, assignee_id) VALUES (?, ?)", (1, 2)) # Assign "Morning meeting" to "Ashley" cur.execute("INSERT INTO item_assignees (item_id, assignee_id) VALUES (?, ?)", (1, 4)) # Assign "Buy fruit" to "Sammy" cur.execute("INSERT INTO item_assignees (item_id, assignee_id) VALUES (?, ?)", (2, 1)) connection.commit() connection.close()
在突出显示的代码中,您通过插入 item_assignees
连接表将待办事项分配给受理人。 您插入要分配给具有与 assignee_id
值对应的 ID 的受让人的待办事项的 item_id
。 在突出显示的第一行中,您将 ID 为 1
的待办事项 Morning meeting
分配给 ID 为 1
。 其余的行遵循相同的模式。 再一次,您使用 ?
占位符将要插入到元组中的值安全地传递给 cur.execute()
方法。
保存并关闭文件。
运行 init_db.py
程序重新初始化数据库:
python init_db.py
运行 list_example.py
程序,显示您在数据库中的待办事项:
python list_example.py
这是输出:
OutputHome Buy fruit | id: 2 | done: 0 Cook dinner | id: 3 | done: 0 Study Learn Flask | id: 4 | done: 0 Learn SQLite | id: 5 | done: 0 Work Morning meeting | id: 1 | done: 0
这会在它们所属的列表下显示待办事项。 您拥有每个项目的内容、ID 以及是否已完成(0
表示项目尚未完成,1
表示已完成)。 您现在需要显示每个待办事项的受让人。
打开 list_example.py
修改它以显示项目受理人:
nano list_example.py
修改文件如下所示:
flask_todo/list_example.py
from itertools import groupby from app import get_db_connection conn = get_db_connection() todos = conn.execute('SELECT i.id, i.done, i.content, l.title \ FROM items i JOIN lists l \ ON i.list_id = l.id ORDER BY l.title;').fetchall() lists = {} for k, g in groupby(todos, key=lambda t: t['title']): # Create an empty list for items items = [] # Go through each to-do item row in the groupby() grouper object for item in g: # Get the assignees of the current to-do item assignees = conn.execute('SELECT a.id, a.name FROM assignees a \ JOIN item_assignees i_a \ ON a.id = i_a.assignee_id \ WHERE i_a.item_id = ?', (item['id'],)).fetchall() # Convert the item row into a dictionary to add assignees item = dict(item) item['assignees'] = assignees items.append(item) # Build the list of dictionaries # the list's name (ex: Home/Study/Work) as the key # and a list of dictionaries of to-do items # belonging to that list as the value lists[k] = list(items) for list_, items in lists.items(): print(list_) for item in items: assignee_names = ', '.join(a['name'] for a in item['assignees']) print(' ', item['content'], '| id:', item['id'], '| done:', item['done'], '| assignees:', assignee_names)
保存并关闭文件。
您可以使用 groupby() 函数按待办事项所属列表的标题对它们进行分组。 (有关更多信息,请参阅 How To Use One-to-Many Database Relations with Flask and SQLite 的 Step 2。)在进行分组过程时,您会创建一个名为 [ X198X],它将保存所有待办事项数据,例如项目的 ID、内容和受让人。 接下来,在 for item in g
循环中,您遍历每个待办事项,获取该项目的受让人,并将其保存在 assignees
变量中。
assignees
变量保存 SELECT
SQL 查询的结果。 此查询从 assignees
表(别名为 a
)中获取受让人的 id (a.id
) 和受让人的名称 (a.name
) 以缩短查询)。 该查询将 id 和 name 与 item_assignees
连接表(别名为 i_a
)在条件 a.id = i_a.assignee_id
上,其中 i_a.item_id
值等于当前的值项目的 ID (item['id']
)。 然后使用 fetchall() 方法将结果作为列表获取。
使用 item = dict(item)
行,您可以将项目转换为字典,因为常规的 sqlite3.Row 对象不支持分配,您需要向项目添加受让人。 接下来,使用 item['assignees'] = assignees
行,将新键 'assignees'
添加到 item
字典中,以直接从项目的字典中访问项目的受让人。 然后将修改后的项目附加到 items
列表中。 您构建将包含所有数据的字典列表; 每个字典键是待办事项列表的标题,它的值是属于它的所有项目的列表。
要打印结果,使用 for list_, items in lists.items()
循环遍历每个待办事项列表标题和属于它的待办事项,打印列表的标题 (list_
),然后遍历列表的待办事项。 您添加了一个名为 assignee_names
的变量,其值使用 join() 方法 将 生成器表达式 a['name'] for a in item['assignees']
的项目之间连接起来,它从 item['assignees']
列表中每个受让人的数据中提取受让人的名称(a['name']
)。 这个加入的受让人姓名列表,然后您可以在 print()
函数中打印待办事项的其余数据。
运行list_example.py
程序:
python list_example.py
这是输出(突出显示受让人):
OutputHome Buy fruit | id: 2 | done: 0 | assignees: Sammy Cook dinner | id: 3 | done: 0 | assignees: Study Learn Flask | id: 4 | done: 0 | assignees: Learn SQLite | id: 5 | done: 0 | assignees: Work Morning meeting | id: 1 | done: 0 | assignees: Sammy, Jo, Ashley
您现在可以将每个待办事项的受让人与其余数据一起显示。
您现在已显示每个待办事项的受让人名称。 接下来,您将使用它在 Web 应用程序的索引页面中的每个待办事项下方显示名称。
第 4 步 - 在索引页面中显示受让人
在此步骤中,您将修改待办事项管理应用程序的索引页面以显示每个待办事项的受理人。 您将首先编辑包含 Flask 应用程序代码的 app.py
文件,然后编辑 index.html
模板文件以在索引页面上的每个待办事项下方显示受让人。
首先打开app.py
编辑index()
视图功能:
nano app.py
修改函数如下所示:
烧瓶待办事项/app.py
@app.route('/') def index(): conn = get_db_connection() todos = conn.execute('SELECT i.id, i.done, i.content, l.title \ FROM items i JOIN lists l \ ON i.list_id = l.id ORDER BY l.title;').fetchall() lists = {} for k, g in groupby(todos, key=lambda t: t['title']): # Create an empty list for items items = [] # Go through each to-do item row in the groupby() grouper object for item in g: # Get the assignees of the current to-do item assignees = conn.execute('SELECT a.id, a.name FROM assignees a \ JOIN item_assignees i_a \ ON a.id = i_a.assignee_id \ WHERE i_a.item_id = ?', (item['id'],)).fetchall() # Convert the item row into a dictionary to add assignees item = dict(item) item['assignees'] = assignees items.append(item) # Build the list of dictionaries # the list's name (ex: Home/Study/Work) as the key # and a list of dictionaries of to-do items # belonging to that list as the value lists[k] = list(items) conn.close() return render_template('index.html', lists=lists)
保存并关闭文件。
这与您在 Step 3 中的 list_example.py
演示程序中使用的代码相同。 这样,lists
变量将包含您需要的所有数据,包括受让人数据,您将使用这些数据访问 index.html
模板文件中的受让人名称。
打开 index.html
文件以在每个项目后面添加受让人名称:
nano templates/index.html
修改文件如下所示:
flask_todo/templates/index.html
{% extends 'base.html' %} {% block content %} <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1> {% for list, items in lists.items() %} <div class="card" style="width: 18rem; margin-bottom: 50px;"> <div class="card-header"> <h3>{{ list }}</h3> </div> <ul class="list-group list-group-flush"> {% for item in items %} <li class="list-group-item" {% if item['done'] %} style="text-decoration: line-through;" {% endif %} >{{ item['content'] }} {% if not item ['done'] %} {% set URL = 'do' %} {% set BUTTON = 'Do' %} {% else %} {% set URL = 'undo' %} {% set BUTTON = 'Undo' %} {% endif %} <div class="row"> <div class="col-12 col-md-3"> <form action="{{ url_for(URL, id=item['id']) }}" method="POST"> <input type="submit" value="{{ BUTTON }}" class="btn btn-success btn-sm"> </form> </div> <div class="col-12 col-md-3"> <a class="btn btn-warning btn-sm" href="{{ url_for('edit', id=item['id']) }}">Edit</a> </div> <div class="col-12 col-md-3"> <form action="{{ url_for('delete', id=item['id']) }}" method="POST"> <input type="submit" value="Delete" class="btn btn-danger btn-sm"> </form> </div> </div> <hr> {% if item['assignees'] %} <span style="color: #6a6a6a">Assigned to</span> {% for assignee in item['assignees'] %} <span class="badge badge-primary"> {{ assignee['name'] }} </span> {% endfor %} {% endif %} </li> {% endfor %} </ul> </div> {% endfor %} {% endblock %}
保存并关闭文件。
通过此修改,您使用 <hr>
标签在每个项目下方添加了一个换行符。 如果项目有任何受让人(通过语句 if item['assignees']
知道),则显示灰色的 Assigned to
文本并遍历项目受让人(即 item['assignees']
列表),并在 badge 中显示受让人名称 (assignee['name']
)。
最后,运行开发服务器:
flask run
然后访问索引页面:http://127.0.0.1:5000/
。
每个待办事项现在可以有多个受理人,您可以为每个受理人分配多个待办事项。 索引页面显示所有项目和每个项目的受让人。
您可以从此存储库 访问最终代码 。
结论
在本教程中,您了解了什么是 多对多 关系,如何在 Flask 和 SQLite Web 应用程序中使用它,如何在表之间连接,以及如何在 Python 中对关系数据进行分组。
您现在拥有一个完整的待办事项应用程序,用户可以在其中创建新的待办事项、将项目标记为完成、编辑或删除现有项目以及创建新列表。 并且每个项目都可以分配给不同的受让人。
要了解有关使用 Python 和 Flask 进行 Web 开发的更多信息,请参阅这些 Flask 教程。