注意:查看解决方案的解决方案...长话短说,如果表单之间存在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。
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()]
@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 %}
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知识可以忽略不计。
继我的评论之后,不推荐你的做法,因为你试图在两条路线上坚持数据库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中涉及一对多数据库关系和外键依赖关系的多步骤表单。
一些上下文:我正在创建一个“事件集”,它的子节点(“事件”)和结果集(每个事件的子节点)。
首先,根据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()