算法多对多分配数据是指将一组源对象与一组目标对象通过算法进行匹配,从而将源对象以某种关系分配给目标对象的过程。典型的算法多对多分配数据模型有最大分配模型和最小差异模型,它们由一组源对象、一组目标对象和一个评分矩阵组成。源对象和目标对象可以是任何实现目标一致的单位。在最大分配模型中,评分矩阵中的值表示每一源-目标对象分配的贡献,而算法将会根据这些值找到一个最佳分配方案,使用者也可以调整分配权重参数来确保结果更接近自己的需求。在最小差异模型中,评分矩阵中的值表示每一源-目标对象分配的差异程度,算法会找到一个最佳分配方案,使得每一源-目标对象中的分配差异最小。不论是何种模型,算法都将根据该算法要求的分配权重参数有选择性的分配源对象和目标对象。比如当评分矩阵中的值越大,则说明源-目标对象的分配贡献或差异程度越大,那么分配权重参数就越大,其他对象需要被分配的机会就越少。
有许多多对多分配的算法,其中包括动态规划、多级索引、增广路等,不同的算法有不同的特点,也有不同的优势。动态规划有很高的效率,可以克服复杂约束问题,并且有利于分配最大值。多级索引法可以快速查找到最优解,而增广路算法更关注每一个源-目标对象的分配细节,可以有效的克服约束条件,满足实际情况。这些算法有助于更好的实施多对多分配,并且在复杂的任务中也能取得较好的分配效果。
Flask是一个使用 Python 语言构建 Web 应用程序的框架,而SQLite是一个数据库引擎,可以与 Python 一起使用来存储应用程序数据。在本教程中,您将通过添加多对多关系来修改使用 Flask 和 SQLite 构建的应用程序。
虽然您可以独立学习本教程,但它也是如何使用 Flask 和 SQLite 修改一对多数据库关系中的项目教程的延续,在该教程中,我们管理具有一对多关系的多表数据库使用待办事项应用程序示例。该应用程序允许用户添加新的待办事项、在不同列表下对项目进行分类以及修改项目。
一个多一对多数据库关系是两个表其中每个表中的记录可以在其它引用表中的记录数之间的关系。例如,在博客中,帖子表可以与存储作者的表存在多对多关系。每个帖子可以引用多个作者,每个作者可以引用多个帖子。每个帖子可以有很多作者,每个作者可以写很多帖子。因此,帖子和作者之间存在多对多关系。再比如,在社交媒体应用中,每个帖子可能有多个hashtag,每个hashtag可能有多个post。
在本教程结束时,您的应用程序将拥有一项新功能,用于将待办事项分配给不同的用户。我们将使用单词assignees来指代被分配待办事项的用户。例如,您可以为 有一个家庭待办事项Cleaning the kitchen,您可以将其分配给Sammy和Jo– 每个待办事项可以有多个受让人(即Sammy和Jo)。并且每个用户可以分配多个to-dos(即Sammy可以分配多个to-do 项),这是to-do 项和受让人之间的多对多关系。
在本教程结束时,应用程序将包含一个Assigned to标签,其中列出了受让人的姓名。
在开始遵循本指南之前,您需要:
本地 Python 3 编程环境,请遵循如何为 Python 3系列安装和设置本地编程环境中的分发教程。在本教程中,我们将调用我们的项目目录flask_todo。
(可选)在第 1 步中,您可以选择克隆您将在本教程中处理的待办事项应用程序。但是,您可以选择学习如何使用 Flask 和 SQLite 的一对多数据库关系以及如何使用 Flask 和 SQLite修改一对多数据库关系中的项目。您可以从此页面访问最终代码。
(可选)了解基本的 Flask 概念,例如创建路由、呈现 HTML 模板和连接到 SQLite 数据库。如果您不熟悉这些概念,请查看如何在 Python 3 中使用 Flask 制作 Web 应用程序和如何在 Python 3 中使用 sqlite3 模块,但这不是必需的。
在此步骤中,您将设置待办事项应用程序以备修改。您还将查看数据库架构以了解数据库的结构。如果您按照先决条件部分中的教程进行操作,并且本地计算机上仍有代码和虚拟环境,则可以跳过此步骤。
为了演示向 Flask Web 应用程序添加多对多关系,您将使用上一教程的应用程序代码,这是一个使用Flask、SQLite和Bootstrap 框架构建的待办事项管理 Web 应用程序。使用此应用程序,用户可以创建新的待办事项、修改和删除现有的待办事项以及将待办事项标记为已完成。
使用以下命令克隆存储库并将其重命名flask-todo-2为flask_todo:
导航到flask_todo:
然后创建一个新的虚拟环境:
激活环境:
安装烧瓶:
然后,使用init_db.py程序初始化数据库:
接下来,设置以下环境变量:
FLASK_APP表示您当前正在开发的应用程序,app.py在这种情况下。FLASK_ENV指定模式——设置development为开发模式;这将允许您调试应用程序。(切记不要在生产环境中使用这种模式。)
然后运行开发服务器:
如果您转到浏览器,您将在以下 URL 上运行该应用程序:http://127.0.0.1:5000/。
要停止开发服务器,请使用CTRL + C.
接下来,您将通过数据库模式了解表之间的当前关系。如果您熟悉该schema.sql文件的内容,则可以跳到下一步。
打开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表包含以下列:
该items表包含以下列:
在items表中有一个外键约束,其中的list_id列引用父表的id列lists。这是items和lists的一对多关系,表示一个list可以有多个items,items属于一个list:
FOREIGN KEY (list_id) REFERENCES lists (id)
在下一步中,您将使用多对多关系在两个表之间创建链接。
在此步骤中,您将回顾如何实现多对多关系和联接表。然后,您将添加一个用于存储受让人的新表。
多对多关系链接两个表,其中一个表中的每个项目在另一个表中都有许多相关项目。
假设您有一个简单的待办事项表,如下所示:
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:
assignees如果表已存在,则添加一行以删除该表。这是为了避免重新启动数据库时潜在的未来问题,例如assignees具有不同列的现有表,如果它不遵循相同的架构,可能会意外破坏代码。您还为表添加 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表包含以下列:
编辑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()
保存并关闭文件。
在突出显示的行中,您使用游标对象执行INSERTSQL 语句以将四个名称插入assignees表中。您?在execute()方法中使用占位符并传递包含受让人姓名的元组以安全地将数据插入数据库。然后使用 提交事务connection.commit()并关闭连接connection.close()。
这将添加四个受让人数据库,名称Sammy,Jo,Charlie,和Ashley。
运行init_db.py程序重新初始化数据库:
您现在有一个用于在数据库中存储受让人的表。接下来,您将添加一个连接表以在项目和受让人之间创建多对多关系。
在此步骤中,您将使用联接表将待办事项与受让人联系起来。首先,您将编辑数据库模式文件以添加新的连接表,编辑数据库初始化程序以添加一些分配,然后使用演示程序显示每个待办事项的分配对象。
打开schema.sql以添加新表:
由于该表连接了项目和受让人,因此您将称其为item_assignees。添加一行以删除已存在的表,然后为表本身添加 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表包含以下列:
该item_assignees表还具有两个外键约束:一个链接item_id与列id的列items表,另外一个之间的链接assignee_id与列id的列assignees表。
打开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连接表将待办事项分配给受让人。您插入item_id要分配给具有与assignee_id值对应的 ID 的受托人的待办事项的。在第一个突出显示的行中,您将Morning meetingID为 的待办事项分配给ID1为 的受理Sammy人1。其余的行遵循相同的模式。再一次,您使用?占位符将要插入元组的值安全地传递给cur.execute()方法。
保存并关闭文件。
运行init_db.py程序重新初始化数据库:
运行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修改它以显示项目受让人:
修改文件如下:
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()函数根据待办事项所属的列表的标题对它们进行分组。(见第2步的如何使用一个一对多数据库关系与瓶和SQLite以获取更多信息)。虽然经历了分组过程中,您创建一个名为一个空列表items,这将容纳所有待完成产品的数据,例如项目的 ID、内容和受让人。接下来,在for item in g循环中,您遍历每个待办事项,获取该项目的受让人,并将其保存在assignees变量中。
该assignees变量保存SELECTSQL 查询的结果。此查询从表中获取受让人的 id ( a.id) 和受让人的姓名 ( a.name) assignees(别名为a以缩短查询)。在值等于当前项目的 ID ( )的条件下,查询将 id 和 name 与item_assignees连接表(别名为i_a)连接起来。然后使用该方法以列表形式获取结果。a.id = i_a.assignee_idi_a.item_iditem['id']fetchall()
使用 line 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']提取受让人的姓名 ( a['name']) item['assignees']。这个加入的受让人姓名列表,然后您可以在print()函数中打印剩余的待办事项数据。
运行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 应用程序索引页面中每个待办事项下方的名称。
在此步骤中,您将修改待办事项管理应用程序的索引页面以显示每个待办事项的受理人。您将首先编辑app.py包含 Flask 应用程序代码的文件,然后编辑index.html模板文件以在索引页面上的每个待办事项下方显示受让人。
首先,打开app.py编辑index()视图功能:
修改函数如下:
@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)
保存并关闭文件。
这与您list_example.py在第 3 步的演示程序中使用的代码相同。这样,该lists变量将包含您需要的所有数据,包括受让人数据,您将使用这些数据来访问index.html模板文件中的受让人名称。
打开index.html文件以在每个项目后添加受理人姓名:
修改文件如下:
{% extends 'base.html' %} {% block content %}{% block title %} Welcome to FlaskTodo {% endblock %}
{% for list, items in lists.items() %}{% endfor %} {% endblock %}{{ list }}
{% for item in items %}
- {% 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 %}
{% endfor %}
{% if item['assignees'] %} Assigned to {% for assignee in item['assignees'] %} {{ assignee['name'] }} {% endfor %} {% endif %}
保存并关闭文件。
通过此修改,您使用
最后,运行开发服务器:
然后访问索引页:http://127.0.0.1:5000/。
每个待办事项现在可以有多个受托人,您可以为每个受托人分配多个待办事项。索引页显示所有项目和每个项目的受让人。
您可以从此存储库访问最终代码。
在本教程中,您学习了多对多关系是什么,如何在 Flask 和 SQLite Web 应用程序中使用它,如何在表之间连接,以及如何在 Python 中对关系数据进行分组。
您现在拥有一个完整的待办事项应用程序,用户可以在其中创建新的待办事项项目、将项目标记为完成、编辑或删除现有项目以及创建新列表。每个项目都可以分配给不同的受让人。
要了解有关使用 Python 和 Flask 进行 Web 开发的更多信息,请参阅这些Flask 教程。