烧瓶表单:如何在一次提交中使用多种表单将父实体和子实体添加到数据库?

时间:2019-04-12 06:38:46

标签: python flask sqlalchemy flask-sqlalchemy flask-wtforms

注意:查看解决方案的答案... 长话短说,在相同的数据库会话中,不能“使用一次提交中的多种形式将父项和子项实体添加到DB中”如果表单之间存在HTTP请求,则为SQLAlchemy。对于我的用例而言,适当的方法是将我的多个表单的输出保存在Flask会话中,然后在单个视图中遍历该会话以进行数据库提交。

原始问题:

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

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

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

我正在尝试通过第一步通过Flask会话传递SQLAlchemy为事件集生成的ID。 **我能够成功地将Eventset_id添加到我的Flask会话中,并验证SQLAlchemy会话是否处于活动状态,但是第二步中创建的任何事件都无法识别已刷新(但未提交)的事件集,并最终以{{ 1}}。

我想避免从第一步开始提交事件集,因为我不希望用户在未完成完整的设置过程(即创建事件集和 n 活动)。

forms.py

eventset_id = NONE

views.py

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

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()]

eventset_setup.html

@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)

端子输出

{% 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 %}

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

理想情况下,我希望使用户能够通过一次SQLAlchemy提交来创建事件集和相关的事件(如果我能完成一个Eventet:Event的创建,那么我可以弄清楚添加多个Event)。当前,我的代码导致将Eventset.id值写入会话,并且在没有预期的Eventset父级的情况下创建了Event并将其提交给db。我非常希望避免使用隐藏的表单域来完成此操作,但是不幸的是,我的Javascript知识可以忽略不计。

2 个答案:

答案 0 :(得分: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()

答案 1 :(得分: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数据库会话获取其ID,在数据库中创建flush。然后,我遍历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()