Почему этот пример SQLAlchemy фиксирует изменения в БД? - PullRequest
4 голосов
/ 22 октября 2010

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

Ответы [ 5 ]

5 голосов
/ 26 октября 2010

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()
1 голос
/ 28 октября 2010

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

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

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

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

1 голос
/ 28 октября 2010

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

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

show create table users;

Вы можете изменить механизм БД для таблицы с таблицей alter:

alter table users engine="InnoDB";
0 голосов
/ 26 октября 2010

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

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

0 голосов
/ 22 октября 2010

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

...