6

SQLAlchemy: В чем разница между flush() и commit()?

1

В чем разница между flush() и commit() в SQLAlchemy?

Я уже читал документацию, но не стал wiser - она, кажется, предполагает предысторию, которой у меня нет.

Меня особенно интересует их влияние на использование памяти. Я загружаю данные в базу данных из серии файлов (всего около 5 миллионов строк), и моя сессия иногда “падает” - это большая база данных и машина с ограниченной памятью.

Я начинаю сомневаться, не вызываю ли я слишком много commit() и не слишком ли мало flush() - но без четкого понимания различий мне сложно это определить!

4 ответ(ов)

0

Используйте flush, когда вам нужно смоделировать запись, например, чтобы получить ID первичного ключа из счетчика с автоинкрементом.

john = Person(name='John Smith', parent=None)
session.add(john)
session.flush()

son = Person(name='Bill Smith', parent=john.id)

Без вызова flush john.id будет равен null.

Как уже упоминали другие, без вызова commit() никакие изменения не будут постоянно сохранены в базе данных.

0

Существующие ответы могут показаться не очень понятными, если не понимать, что такое транзакция в базе данных. (Это касалось и меня до недавнего времени.)

Иногда вам может потребоваться выполнить несколько SQL-запросов так, чтобы они были успешными или проваливались вместе. Например, если вы хотите выполнить банковский перевод с счета A на счет B, вам нужно сделать два запроса:

UPDATE accounts SET value = value - 100 WHERE acct = 'A'
UPDATE accounts SET value = value + 100 WHERE acct = 'B'

Если первый запрос выполнится успешно, а второй потерпит неудачу, это плохо (по очевидным причинам). Поэтому нам нужно иметь возможность рассматривать эти два запроса "вместе". Решение заключается в том, чтобы начать с оператора BEGIN и закончить либо оператором COMMIT, либо оператором ROLLBACK, например:

BEGIN
UPDATE accounts SET value = value - 100 WHERE acct = 'A'
UPDATE accounts SET value = value + 100 WHERE acct = 'B'
COMMIT

Это одна транзакция.

В ORM SQLAlchemy это может выглядеть так:

                                      # BEGIN начинается здесь
acctA = session.query(Account).get(1) # SELECT выполняется здесь
acctB = session.query(Account).get(2) # SELECT выполняется здесь

acctA.value -= 100
acctB.value += 100

session.commit()                      # UPDATE и COMMIT выполняются здесь 

Если вы будете следить за тем, когда выполняются различные запросы, вы заметите, что UPDATE не отправляется в базу данных, пока вы не вызовете session.commit().

В некоторых ситуациях может потребоваться выполнить операцию UPDATE перед тем, как вызвать COMMIT. (Возможно, база данных назначает объекту автоинкрементный идентификатор, и вам нужно получить его до выполнения COMMIT). В таких случаях вы можете явно использовать flush() для сеанса.

                                      # BEGIN начинается здесь
acctA = session.query(Account).get(1) # SELECT выполняется здесь
acctB = session.query(Account).get(2) # SELECT выполняется здесь

acctA.value -= 100
acctB.value += 100

session.flush()                       # UPDATE выполняются здесь 
session.commit()                      # COMMIT выполняется здесь 
0

Для простоты понимания:

  • commit делает реальные изменения (они становятся видимыми в базе данных)
  • flush делает фиктивные изменения (они становятся видимыми только для вас)

Представьте себе, что базы данных работают по принципу ветвления, как в git:

  • Прежде всего, нужно понять, что во время transaction вы не манипулируете реальными данными базы данных.
  • Вместо этого вы получаете нечто вроде новой branch, где можете экспериментировать.
  • Если в какой-то момент вы выполните команду commit, это будет означать: "слить мои изменения данных в основную БД".
  • Но если вам нужны определенные данные в будущем, которые вы сможете получить только после commit (например, вставка в таблицу, и вам нужен вставленный PKID), тогда вы используете команду flush, что означает: "вычислите мне будущий PKID, и зарезервируйте его для меня". Затем вы можете использовать это значение PKID в вашем коде и быть уверенными в том, что реальные данные будут такими, как ожидалось.
  • Commit должен всегда происходить в конце, чтобы слить изменения в основную БД.
0

Когда вы вызываете flush(), вы сообщаете базе данных о некоторых изменениях, которые хотите внести (добавление данных, удаление данных, редактирование данных и т.д.), но сама база данных не применяет эти изменения. Вместо этого база данных соглашается притвориться, что эти изменения были выполнены. Если после выполнения flush() вы отправите новый запрос, то увидите свои новые данные в результатах, поскольку база данных показывает, как она выглядит, если бы ваши изменения были применены. Однако на самом деле они еще не были внесены.

Это означает, что flush() по сути является локальным, или частным, действием. В реальном приложении с работающей базой данных может быть много разных пользователей (или процессов), которые одновременно отправляют запросы. Если какой-то другой процесс выполнит такой же запрос, как вы, он увидит другие результаты. Другой процесс не сможет видеть ваши новые данные, потому что реальная база данных еще не была обновлена, и ваши изменения являются лишь частью вашей транзакции. У другого процесса будет своя транзакция, и ваши изменения не входят в нее.

Когда вы вызываете commit(), вы сообщаете базе данных фактически выполнить изменения и обновить реальную базу данных. После этого другие пользователи смогут видеть новые данные.

(Обратите внимание, что SQLAlchemy автоматически выполнит flush перед commit, независимо от того, вызываете ли вы flush() явно или нет.)

(Также отмечу, что, как упоминали другие: это просто модель, которую использует SQLAlchemy. Это то, как SQLAlchemy ожидает, что будет работать база данных. Но некоторые системы баз данных не используют транзакции, и если вы работаете с такими системами, то flush действительно может обновить реальные данные.)

Но почему?

Иногда нужно выполнить несколько изменений в одном запросе, чтобы поддерживать базу данных в действительном состоянии.

Например, предположим, ваша организация покупает новый автомобиль, чтобы заменить старый, и сотрудник по имени Сьюзен назначен для его обслуживания. Вам нужно сделать три вещи:

  1. Вставить новую запись о новом автомобиле в таблицу cars.
  2. Изменить запись Сьюзен в таблице employees, чтобы показать, что она больше не отвечает за старый автомобиль и теперь назначена на новый.
  3. Удалить запись о старом автомобиле из таблицы cars.

Возможно, по каким-то причинам каждому сотруднику необходимо быть назначенным ровно на один автомобиль в любой момент времени, и каждый автомобиль должен быть назначен ровно одному сотруднику. Возможно, какой-то другой процесс сломается, если когда-либо выполнит запрос к базе данных и увидит, что Сьюзен назначена на два автомобиля одновременно, или если обнаружит, что есть автомобиль, на который никто не назначен. Поэтому все три операции должны быть частью одной и той же транзакции и выполнены одновременно, чтобы поддерживать базу данных в действительном состоянии.

И, возможно, по каким-то причинам вам нужно сначала сохранить новый автомобиль в базе данных, прежде чем вы сможете назначить Сьюзен на него (возможно, вам нужно знать идентификатор нового автомобиля, чтобы включить его в запись Сьюзен). Теперь у вас имеется своего рода проблема "курицы и яйца". Вы не можете выполнить шаг 2, пока не выполните шаг 1, но вы не можете выполнить шаг 1, если не сделаете это одновременно со шагами 2 и 3.

Решение заключается в том, чтобы вызвать flush() после первого шага. Это отправит ваши новые данные в систему базы данных и предоставит вам действительный идентификационный номер для нового автомобиля, не редактируя реальные данные. Затем вы можете использовать новый идентификатор и выполнить остальные два шага, и, наконец, пришло время вызвать commit(). Для вас это выглядит как многоступенчатый процесс, но с точки зрения других пользователей, все три изменения будут применены к базе данных одновременно, и база данных никогда не останется в недействительном состоянии.

(Обратите внимание, что по умолчанию SQLAlchemy автоматически выполнит flush перед выполнением запроса, поэтому вам может не понадобиться явно вызывать flush().)

Да, у SQLAlchemy есть и другие более эффективные способы обработки подобных примеров, но я думаю, что это поясняет суть.

Что насчет использования памяти?

К сожалению, я не знаю ничего о использовании памяти. Моего понимания недостаточно для глубокого анализа. Вас интересовала сторона базы данных? Или клиентская сторона?

С точки зрения базы данных, я предполагаю, что действительно крупная транзакция использует много памяти, потому что системе нужно хранить оригинальную базу данных наряду со всеми изменениями и правками. Если изменения включают много правок или удалений, то выполнение коммита, вероятно, освободит немного памяти.

Что касается клиентской стороны, то у меня нет информации.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь