Почему этот пример SQLAlchemy фиксирует изменения в БД?

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

Мой конкретный вопрос можно найти в комментариях к коду. Основная цель состоит в том, чтобы иметь более четкое представление о том, когда и почему SQLAlchemy будет фиксироваться в БД.

Мой более широкий вопрос заключается в том, должно ли мое приложение (а) использовать глобальный экземпляр Session или (б) использовать глобальный класс Session, из которого будут создаваться экземпляры конкретных экземпляров. Основываясь на этом примере, я начинаю думать, что правильный ответ (б). Это правильно? Изменить: эта документация SQLAlchemy предполагает, что (b) рекомендуется.

import sys

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id   = Column(Integer, primary_key = True)
    name = Column(String)
    age  = Column(Integer)

    def __init__(self, name, age = 0):
        self.name = name
        self.age  = 0

    def __repr__(self):
        return "<User(name='{0}', age={1})>".format(self.name, self.age)

engine = create_engine('sqlite://', echo = False)
Base.metadata.create_all(engine)

Session = sessionmaker()
Session.configure(bind=engine)

global_session = Session() # A global Session instance.
commit_ages    = False     # Whether to commit in modify_ages().
use_global     = True      # If True, modify_ages() will commit, regardless
                           # of the value of commit_ages. Why?

def get_session():
    return global_session if use_global else Session()

def add_users(names):
    s = get_session()
    s.add_all(User(nm) for nm in names)
    s.commit()

def list_users():
    s = get_session()
    for u in s.query(User): print ' ', u

def modify_ages():
    s = get_session()
    n = 0
    for u in s.query(User):
        n += 10
        u.age = n
    if commit_ages: s.commit()

add_users(('A', 'B', 'C'))
print '\nBefore:'
list_users()
modify_ages()
print '\nAfter:'
list_users()

person FMc    schedule 22.10.2010    source источник
comment
Я попытался ответить на ваш конкретный вопрос, но да, в отношении вашего Правка: я бы определенно пошел по пути (b). Экземпляр глобального сеанса просто запрашивает проблемы, точно такие же, как этот.   -  person snapshoe    schedule 26.10.2010


Ответы (5)


tl;dr — Обновления не на самом деле зафиксированы в базе данных — они являются частью незафиксированной транзакции.


Я внес 2 отдельных изменения в ваш вызов create_engine(). (Кроме этой одной строки, я использую ваш код точно так, как он опубликован.)

Первый был

engine = create_engine('sqlite://', echo = True)

Это дает некоторую полезную информацию. Я не собираюсь публиковать здесь весь вывод, но обратите внимание, что никакие команды обновления SQL не выдаются до после второго вызова list_users():

...
After:
xxxx-xx-xx xx:xx:xx,xxx INFO sqlalchemy.engine.base.Engine.0x...d3d0 UPDATE users SET age=? WHERE users.id = ?
xxxx-xx-xx xx:xx:xx,xxx INFO sqlalchemy.engine.base.Engine.0x...d3d0 (10, 1)
...

Это признак того, что данные не сохраняются, а хранятся в объекте сеанса.

Второе изменение, которое я сделал, состояло в том, чтобы сохранить базу данных в файл с

engine = create_engine('sqlite:///db.sqlite', echo = True)

Повторный запуск скрипта дает тот же результат, что и раньше, для второго вызова list_users():

<User(name='A', age=10)>
<User(name='B', age=20)>
<User(name='C', age=30)>

Однако, если вы теперь откроете базу данных, которую мы только что создали, и запросите ее содержимое, вы увидите, что добавленные пользователи были сохранены в базе данных, а возрастные модификации — нет:

$ sqlite3 db.sqlite "select * from users"
1|A|0
2|B|0
3|C|0

Таким образом, второй вызов list_users() получает значения из объекта сеанса, а не из базы данных, поскольку выполняется транзакция, которая еще не была зафиксирована. Чтобы доказать это, добавьте следующие строки в конец вашего скрипта:

s = get_session()
s.rollback()
print '\nAfter rollback:'
list_users()
person snapshoe    schedule 26.10.2010
comment
Большое спасибо. Это похоже на правильный ответ. К сожалению, это означает, что мой пример сценария не демонстрирует проблему, наблюдаемую в моем полном приложении, которое действительно фиксирует изменения в БД MySQL, даже если я не вызываю commit(). Время провести дополнительное расследование! - person FMc; 26.10.2010
comment
Я бы сказал, что простое использование echo=True для вызова create_engine во время отладки было бы одним из лучших инструментов для определения того, что и когда происходит. - person snapshoe; 27.10.2010

Поскольку вы заявляете, что на самом деле используете MySQL в системе, в которой вы видите проблему, проверьте тип механизма, с которым была создана таблица. По умолчанию используется MyISAM, который не поддерживает ACID-транзакции. Убедитесь, что вы используете движок InnoDB, который выполняет ACID-транзакции.

Вы можете увидеть, какой движок использует таблица с

show create table users;

Вы можете изменить механизм базы данных для таблицы с помощью alter table:

alter table users engine="InnoDB";
person Nathan Davis    schedule 27.10.2010

<сильный>1. пример: Просто чтобы убедиться, что (или проверить, что) сеанс не фиксирует изменения, достаточно вызвать expunge_all в объекте сеанса. Скорее всего, это докажет, что изменения не на самом деле зафиксированы:

....
print '\nAfter:'
get_session().expunge_all()
list_users()

<сильный>2. mysql: Как вы уже упоминали, пример sqlite может не отражать то, что вы на самом деле видите при использовании mysql. Как описано в sqlalchemy — MySQL — Механизмы хранения. , наиболее вероятной причиной вашей проблемы является использование нетранзакционных механизмов хранения (таких как MyISAM), что приводит к режиму выполнения autocommit.

<сильный>3. область действия сеанса: хотя наличие одного глобального сеанса звучит как поиск проблемы, использование нового сеанса для каждого крошечного запроса также не является хорошей идеей. Сеанс следует рассматривать как транзакцию/единицу работы. Я считаю, что использование контекстных сеансов лучший из двух миров, где вам не нужно передавать объект сеанса в иерархии вызовов методов, и в то же время вам предоставляется довольно неплохая безопасность в многопоточной среде. Время от времени я использую сеанс local, когда знаю, что не хочу взаимодействовать с текущей транзакцией (сеансом).

person van    schedule 28.10.2010
comment
Спасибо. Это очень полезно. - person FMc; 28.10.2010

Обратите внимание, что значения по умолчанию для create_session() противоположны значениям для sessionmaker(): autoflush и expire_on_commit равны False, autocommit — True.

person Paulo Scardine    schedule 22.10.2010
comment
Спасибо за ответ, но я не понимаю, как это решает вопрос. - person FMc; 23.10.2010
comment
Попробуйте sessionmaker() вместо create_session(). - person Paulo Scardine; 23.10.2010
comment
Насколько я могу судить, я уже использую sessionmaker(), а не create_session(). Я что-то упускаю? - person FMc; 23.10.2010

global_session уже создается, когда вы вызываете modify_ages(), и вы уже зафиксировали в базе данных. Если вы повторно создаете экземпляр global_session после фиксации, он должен начать новую транзакцию.

Я предполагаю, что, поскольку вы уже зафиксировали и повторно используете один и тот же объект, каждая дополнительная модификация фиксируется автоматически.

person Scott    schedule 26.10.2010