Flask 形式:如何使用多种形式在一次提交中将父实体和子实体添加到数据库中?

Flask form: how can I use multiple forms to add Parent and Child entities to DB in one commit?

注意:请参阅解决方案的答案... 长话短说,不能 "use multiple forms to add Parent and Child entities to DB in one commit" 在 SQLAlchemy 中使用相同的数据库会话,如果有表单之间的 HTTP 请求。适合我的用例的方法是在 Flask 会话中保存我的多个表单的输出,然后在单个视图中遍历会话以提交数据库。

原题:

TL;DR: 我可以使用 Flask-WTF 表单通过 SQLAlchemy 暂时创建父项吗,db.session.flush() 获取父项的 ID 并将其传递给第二个 Flask-WTF 表单以填充子项的外键,然后在一个 db.session.commit()?

中提交 Parent 和 Child

我正在构建一个 Flask 网络应用程序,使用户能够创建和管理竞争事件。我的数据库模型包括事件和事件集。事件可以是事件集的子项,但事件不需要具有相应的事件集父项。但是,对于用户想要同时创建事件集和相应事件的情况,我想通过两步形式启用此功能(我试图使用两个单独的 flask-wtf 形式和 Flask 视图来实现)。

第一个表单和视图使用户能够创建 Eventset() 的实例。此 Eventset() 被添加到 sqlalchemy 数据库会话并刷新,但未提交。如果表单通过验证,应用程序将重定向到下一个允许创建事件的视图。我想将先前创建的 Eventset 的 ID 传递给我的 Event() 模型以完成父子关系。

我试图通过在第一步中通过 Flask 会话传递由 SQLAlchemy 为 Eventset 生成的 ID 来执行此操作。 **我能够成功地将 Eventset_id 添加到我的 Flask 会话并验证 SQLAlchemy 会话是否处于活动状态,但是在第二步中创建的任何事件都无法识别已刷新(但未提交)的事件集,并最终结束提交给 eventset_id = NONE

我想避免从第一步开始提交事件集,因为我不希望用户在没有完成完整设置过程(即创建事件集和 时无意中创建孤立的事件集n 个事件)。

forms.py

class EventsetsForm(FlaskForm):
    name = StringField("Eventset Name", validators=[DataRequired()])
    submit = SubmitField('Submit')

class EventForm(FlaskForm):
    eventset_id = SelectField('Eventset', validators=[Optional()], coerce=int)
    name = StringField("Event Name", validators=[DataRequired()])
    submit = SubmitField('Submit')

    def __init__(self, *args, **kwargs):
        super(EventForm, self).__init__(*args, **kwargs)
        self.eventset_id.choices = [(0, "---")]+[(eventset.id, eventset.name)
                             for eventset in Eventset.query.order_by(Eventset.name).all()]

views.py

nb:闪烁和打印的消息是为了帮助我了解发生了什么

@main.route('/eventsets/setup/step_one', methods=['GET', 'POST'])
@login_required
@admin_required
def setup_step_one():
    form = EventsetsForm()
    if current_user.can(Permission.WRITE) and form.validate_on_submit():
        eventset = Eventset(name=form.name.data, 
                            author=current_user._get_current_object())
        db.session.add(eventset)
        db.session.flush()
        session['eventset_id'] = eventset.id
        flash('STEP ONE: an eventset named %s has been propped.' % eventset.name)
        flash('STEP ONE: The id from session is: %s' % session['eventset_id'])
        print('STEP ONE: %s' % session['eventset_id'])
        if eventset in db.session:
            print('STEP ONE: sqlalchemy object for eventset is: %s' % eventset)
        return redirect(url_for('.setup_step_two'))
    return render_template('eventset_setup.html', form=form)  

@main.route('/eventsets/setup/step_two', methods=['GET', 'POST'])
@login_required
@admin_required
def setup_step_two():
    print('Is the db session active? %s' % db.session.is_active)
    print('STEP TWO: the eventset id from Flask session should be: %s' % session['eventset_id'])
    eventset_id = int(session['eventset_id'])
    print('STEP TWO: is the eventset_id in the session an int? %s ' % isinstance(eventset_id, int))
    form = EventForm()
    form.eventset_id.data = eventset_id
    if current_user.can(Permission.WRITE) and form.validate_on_submit():
        event = Event(name=form.name.data,
                      author=current_user._get_current_object(),
                      description=form.description.data,
                      location=form.location.data,
                      scheduled=form.scheduled.data,
                      eventset_id=form.eventset_id.data,
                      event_datetime=form.event_datetime.data,
                      open_datetime=form.open_datetime.data)
        db.session.add(event)
        db.session.commit()
        flash('An event named %s has been created, with eventset_id of %s.' % (event.name, event.eventset_id))
        return redirect(url_for('.setup_step_two'))
    return render_template('eventset_setup.html', eventset_id=eventset_id, form=form)

eventset_setup.html

{% block page_content %}
<div class="row">
    <div class="col-md-4">
        {% if session['eventset_id'] != None %}<p>Eventset id should be: {{ session['eventset_id'] }}</p>{% endif %}
        {% if flarg != None %}{{ flarg }}{% endif %}
    </div>
    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
</div>
{% endblock %}

终端输出

127.0.0.1 - - [11/Apr/2019 23:11:34] "GET /eventsets/setup/step_one HTTP/1.1" 200 -
STEP ONE: 54
STEP ONE: sqlalchemy object for eventset is: <app.models.Eventset object at 0x103c4dd30>
127.0.0.1 - - [11/Apr/2019 23:11:38] "POST /eventsets/setup/step_one HTTP/1.1" 302 -
Is the db session active? True
STEP TWO: the eventset id from Flask session should be: 54
STEP TWO: is the eventset_id in the session an int? True
127.0.0.1 - - [11/Apr/2019 23:11:38] "GET /eventsets/setup/step_two HTTP/1.1" 200 -

...然而在此流程中创建的事件导致 event.eventset_id == NONE

理想情况下,我想让用户通过一次 SQLAlchemy 提交创建一个事件集和一个相关事件(如果我得到一个 Eventset:Event 创建工作我可以想出添加多个事件)。目前,我的代码导致将 Eventset.id 值写入会话,并且在没有预期的 Eventset 父级的情况下创建事件并将其提交给数据库。我强烈希望避免使用隐藏的表单字段来完成此操作,不幸的是我的 Javascript 知识微不足道。

根据我的意见,不建议您使用您的方法,因为您正试图通过两条路线持久保存数据库 session。如果浏览器没有遵循第二条路线或被重定向到您网站的其他地方怎么办?然后您将有一个开放的 session,其中包含部分修改的数据,这可能会破坏完整性而阻止提交到数据库。

正如我评论的那样,SQLAlchemy 文档在此处对此进行了更多解释:https://docs.sqlalchemy.org/en/13/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it

如果您使用的是 flask-sqlachemy 那么本页底部 https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/ 说明它已关闭 并在第一条路线(请求)结束时自动回滚您的会话。 flask-sqlachemy 源代码中执行此操作的特定代码行是:

# flask-sqlalchemy source __init__.py lines 805  - 812
@app.teardown_appcontext
def shutdown_session(response_or_exc):
    if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
        if response_or_exc is None:
            self.session.commit()

    self.session.remove()
    return response_or_exc

如果可能使用 session 对象作为存储,而不是将数据添加到数据库,则将其添加到会话,这是实现您想要的更好方法:

session['new_eventset_name'] = form.name.data

然后当你在第二条路线时执行检查:

eventset_name = session.get('new_eventset_name', None)
if eventset_name:
    eventset = Eventset(name=eventset_name, 
                        author=current_user._get_current_object())
    db.session.add(eventset)
    db.session.flush()
    eventset_id = eventset.id
    del session['new_eventset_name']
else:
    eventset_id = None

event = Event(name=form.name.data,
                  author=current_user._get_current_object(),
                  description=form.description.data,
                  location=form.location.data,
                  scheduled=form.scheduled.data,
                  eventset_id=eventset_id,  ## <-- NOTE THIS
                  event_datetime=form.event_datetime.data,
                  open_datetime=form.open_datetime.data)
db.session.add(event)
db.session.commit()

感谢 Attack68 的建议和指导:Flask session 解决了我的问题。在这里,我为其他在 Flask 中处理涉及一对多数据库关系和外键依赖关系的多步骤表单的人发布了我的工作实现。

一些上下文:我正在创建一个 "eventset," 它的子项 ("events") 和结果集(每个事件的子项)。

首先,根据 Attack68 的建议,我使用标准 FlaskForm 创建了一个 eventset,将其保存到会话中: session['new_eventset_name'] = form.name.data

我的下一个视图包括一个用于创建 events 的表单,我将其保存到嵌套字典中的会话中。我为每个条目创建一个唯一的数字键,然后为每个附加事件递增它。

if current_user.can(Permission.WRITE) and form.validate_on_submit():
                if session['new_event_batch'].keys():
                    event_key = str(int(max(session['new_event_batch'].keys())) + 1) 
                else:
                    event_key = 1
                session['new_event_batch'][event_key] = { 'name': form.name.data, 
                                                'description':form.description.data,
                                                'location':form.location.data, 
                                                'scheduled':form.scheduled.data,
                                                'event_datetime':form.event_datetime.data,
                                                'open_datetime':form.open_datetime.data }
                session.modified = True
                return redirect(url_for('.setup_step_two'))

我的下一个视图包含另一个简单的表单,可以创建 resultsets,它将附加到 eventset 中创建的每个 event。它的代码与 event 的代码没有本质区别。

最后,我听从了 Attack68 的建议,在数据库中创建了 eventset,使用 flush 数据库会话来获取它的 ID。然后我遍历 events 的嵌套字典,插入新创建的 eventset.id 作为外键:

eventset = Eventset(name=eventset_name, 
                    author=current_user._get_current_object())
        db.session.add(eventset)
        db.session.flush()
        eventset_id = eventset.id

        event_id_list = []

        for event_key in session['new_event_batch']:

            event = Event(name=session['new_event_batch'][event_key].get('name', ''),
                              author=current_user._get_current_object(),
                              description=session['new_event_batch'][event_key].get('description', ''),
                              location=session['new_event_batch'][event_key].get('location', ''),
                              scheduled=session['new_event_batch'][event_key].get('scheduled', ''),
                              eventset_id=eventset_id,  ## <-- NOTE THIS
                              event_datetime=session['new_event_batch'][event_key].get('event_datetime', ''),
                              open_datetime=session['new_event_batch'][event_key].get('open_datetime', ''))
            db.session.add(event)
            db.session.flush()
            event_id = event.id
            event_id_list.append(event_id)

我还创建了一个新创建的 event.id 值的列表。我随后遍历该列表以创建每个 event resultsets,删除我不再需要的会话值,并将所有内容提交到 db:

        for i in event_id_list:

            for resultset_key in session['new_resultset_batch']:
                resultset = Resultset(name=session['new_resultset_batch'][resultset_key],
                                        author=current_user._get_current_object(),
                                        event_id=i,
                                        last_updated=datetime.utcnow())
                db.session.add(resultset)
                db.session.flush()

        del session['new_eventset_name']
        del session['new_event_batch']
        del session['new_resultset_batch']

        db.session.commit()