Форма колбы: как я могу использовать несколько форм для добавления родительских и дочерних объектов в БД за один коммит? - PullRequest
0 голосов
/ 12 апреля 2019

Примечание: см. Ответы для разрешения ... Короче говоря, нельзя "использовать несколько форм для добавления родительских и дочерних объектов в БД за один коммит", используя один и тот же сеанс базы данных в SQLAlchemy. если между формами есть HTTP-запросы. Подходящий подход для моего варианта использования состоял в том, чтобы сохранить выходные данные моих нескольких форм в сеансе Flask и затем выполнить итерацию по сеансу в одном представлении, чтобы сделать фиксации базы данных.

Оригинальный вопрос:

TL; DR: можно ли использовать форму Flask-WTF для предварительного создания элемента Parent с помощью SQLAlchemy, db.session.flush(), чтобы получить идентификатор родителя и передать его во вторую форму Flask-WTF для заполнения внешнего ключа Child, а затем зафиксировать Родитель и ребенок в одном db.session.commit()?

Я создаю веб-приложение Flask, которое позволяет пользователям создавать и управлять конкурентными событиями. Мои модели базы данных включают в себя события и наборы событий. События могут быть дочерними для Eventset, но для Event не обязательно иметь соответствующего родителя Eventset. Однако для ситуаций, когда пользователи хотят создавать наборы событий и соответствующие события одновременно, я хочу включить это с помощью двухэтапной формы (которую я пытаюсь реализовать с помощью двух отдельных форм flask-wtf и представлений Flask).

Первая форма и представление позволяют пользователю создать экземпляр Eventset (). Этот Eventset () добавляется в сеанс базы данных sqlalchemy и сбрасывается, но не фиксируется. Если форма проверяется, приложение перенаправляет на следующее представление, которое позволяет создать событие. Я хочу передать идентификатор ранее созданного Eventset в мою модель Event (), чтобы завершить отношения Parent-Child.

Я пытаюсь сделать это, передав идентификатор, сгенерированный SQLAlchemy для Eventset, на первом этапе через сеанс Flask. ** Я могу успешно добавить Eventset_id в мой сеанс Flask и убедиться, что сеанс SQLAlchemy активен, но любые события, созданные на втором шаге, не распознают сброшенный (но не зафиксированный) набор событий, и в итоге фиксируются с eventset_id = NONE.

Я хочу избежать фиксации Eventset с первого шага, так как я не хочу, чтобы пользователи непреднамеренно создавали потерянные Eventset, если они не завершили полный процесс установки (т. Е. Создали Eventset и 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 == НЕТ

В идеале, я бы хотел, чтобы пользователи могли создавать набор событий и связанное с ним событие с помощью одной фиксации SQLAlchemy (если я получу один набор событий: создание события работает, я могу выяснить, добавив несколько событий). В настоящее время мой код приводит к тому, что значение Eventset.id записывается в сеанс, а события создаются и фиксируются в БД без ожидаемого родителя Eventset. Я настоятельно предпочитаю избегать использования скрытых полей формы для достижения этой цели, и, к сожалению, мои знания Javascript незначительны.

Ответы [ 2 ]

0 голосов
/ 30 апреля 2019

Все благодарности Attack68 за советы и рекомендации: Flask's 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 сеанс базы данных, чтобы получить его идентификатор. Затем я перебираю вложенный диктант 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 per 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()
0 голосов
/ 20 апреля 2019

В дополнение к моим комментариям, ваш подход не рекомендуется, потому что вы пытаетесь сохранить базу данных 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 / ru / 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()
...