Flask形式:如何在一次提交中使用多个表单将父实体和子实体添加到DB?

问题描述 投票:0回答:2

注意:查看解决方案的解决方案...长话短说,如果表单之间存在HTTP请求,则无法使用SQLAlchemy中的相同数据库会话“使用多个表单在一次提交中将父级和子级实体添加到DB”。我的用例的适当方法是将我的多个表单的输出保存在Flask会话中,然后在单个视图中迭代会话以进行数据库提交。

原始问题:

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

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

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

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

我想避免从第一步提交Eventset,因为我不希望用户在无法完成完整设置过程(即创建Eventset和n Events)时无意中创建孤立的Eventsets。

forms.朋友

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.朋友

nb: the flashed and printed messages are to help me see what's happening

@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和一个相关的事件(如果我得到一个Eventset:事件创建工作,我可以想出添加多个事件)。目前,我的代码导致Eventset.id值被写入会话,并且事件创建并提交给数据库而没有预期的Eventset父级。我强烈希望避免使用隐藏的表单字段来实现这一点,不幸的是我的Javascript知识可以忽略不计。

python flask sqlalchemy flask-sqlalchemy flask-wtforms
2个回答
0
投票

继我的评论之后,不推荐你的做法,因为你试图在两条路线上坚持数据库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()

0
投票

所有归功于Attack68的建议和指导重新:Flask的session来解决我的问题。在这里,我发布了我的工作实现,其中任何其他人都在使用Flask中涉及一对多数据库关系和外键依赖关系的多步骤表单。

一些上下文:我正在创建一个“事件集”,它的子节点(“事件”)和结果集(每个事件的子节点)。

首先,根据Attack68的建议,我使用标准的eventset创建一个FlaskForm,将其保存到会话中: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,它将附加到event中创建的每个eventset。它的代码与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值的列表。我随后遍历该列表以根据resultsets创建event,删除我不再需要的会话值,并将所有内容提交给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()
© www.soinside.com 2019 - 2024. All rights reserved.