Объекты, сохраненные в сеансе SQLAlchemy во время процесса - PullRequest
0 голосов
/ 29 октября 2018

У нас есть несколько функциональных возможностей в нашем веб-приложении Flask, которое состоит из одного вызова функции, который вызывает много подфункций и делает много вещей за кулисами. Например, он добавляет (финансовые) транзакции в базу данных (MSSQL), записывает данные в журнальную таблицу в базе данных и изменяет свойства конкретных объектов, что приводит к изменению столбцов в определенных таблицах в нашей базе данных. Все это делается с помощью SQLAlchemy через объекты.

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

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

if result:
    result.save_to_session(current_app.db_session)
    current_app.db_session.commit()

def save_to_session(self, session):
    session.bulk_save_objects(self.adminlog)
    ...

Этот новый подход привел к ошибке, которую мы не ожидали в строке current_app.db_session.commit(). Кажется, что в конце процесса, когда мы добавляем возвращенные объекты в сеанс и пытаемся зафиксировать сеанс в базе данных, возникает ошибка о дублирующем ключе . Похоже, что во время процесса возвращенные объекты уже были добавлены в сеанс где-то , и SQLAlchemy пытается добавить их дважды.

Мы пришли к такому выводу, потому что когда мы закомментируем вызов bulk_save_objects(), сообщение об ошибке больше не появляется. Однако измененные данные фиксируются в базе данных правильно и точно один раз .

Когда мы проверяем базу данных после возникновения этой ошибки, нет записей с Первичным ключом, упомянутым в сообщении об ошибке. Это из-за отката, который происходит по ошибке. Таким образом, дело не в том, что запись уже существует в базе данных, а в том, что сеанс пытается добавить одну и ту же запись дважды.

Это ошибка, которую мы получаем, используя pymssql в качестве драйвера:

sqlalchemy.exc.IntegrityError: (pymssql.IntegrityError) (2627, 
b"Violation of PRIMARY KEY constraint 'PK_adminlog_id'. 
Cannot insert duplicate key in object 'dbo.adminlog'. 
The duplicate key value is (0E5537FF-E45C-40C5-98FC-7B1ACAD8104E).
DB-Lib error message 20018, severity 14:\n
General SQL Server error: Check messages from the SQL Server\n
") 
[SQL: 
'INSERT INTO adminlog (
    alog_id, 
    alog_ppl_id, 
    alog_user_ppl_id, 
    alog_user_name, 
    alog_datetime, 
    [alog_ipAddress], 
    [alog_macAddress], 
    alog_comment, 
    alog_type, 
    alog_act_id, 
    alog_comp_id, 
    alog_artc_id) 
VALUES (
    %(alog_id)s, 
    %(alog_ppl_id)s, 
    %(alog_user_ppl_id)s, 
    %(alog_user_name)s, 
    %(alog_datetime)s, 
    %(alog_ipAddress)s, 
    %(alog_macAddress)s, 
    %(alog_comment)s, 
    %(alog_type)s, 
    %(alog_act_id)s, 
    %(alog_comp_id)s, 
    %(alog_artc_id)s)'] 

[parameters: (
    {'alog_act_id': None, 
    'alog_comment': 'Le service a été ajouté. Cours Coll (119,88)', 
    'alog_datetime': datetime.datetime(2018, 10, 29, 13, 46, 54, 837178), 
    'alog_macAddress': b'4A-NO-NY-MO-US', 
    'alog_type': b'user', 
    'alog_artc_id': None, 
    'alog_comp_id': None, 
    'alog_id': b'0E5537FF-E45C-40C5-98FC-7B1ACAD8104E', 
    'alog_user_ppl_id': b'99999999-9999-9999-1111-999999999999', 
    'alog_user_name': 'System', 
    'alog_ipAddress': b'0.0.0.0', 
    'alog_ppl_id': b'AE841D1C-5D8D-47F7-B81F-89C5C931BD14'}, 

    {'alog_act_id': None, 
    'alog_comment': 'Le service a été supprimé. 
    01/12/2019 Cours Coll (119,88)', 
    'alog_datetime': datetime.datetime(2018, 10, 29, 13, 46, 55, 71600), 
    'alog_macAddress': b'4A-NO-NY-MO-US', 
    'alog_type': b'user', 
    'alog_artc_id': None, 
    'alog_comp_id': None, 
    'alog_id': b'E22176FB-7490-470F-A8BA-A35D5F55A96A', 
    'alog_user_ppl_id': b'99999999-9999-9999-1111-999999999999', 
    'alog_user_name': 'System', 
    'alog_ipAddress': b'0.0.0.0', 
    'alog_ppl_id': b'AE841D1C-5D8D-47F7-B81F-89C5C931BD14'}
    )]

Мы получаем похожую ошибку, используя PyODBC:

sqlalchemy.exc.IntegrityError: (pyodbc.IntegrityError) ('23000', 
"[23000] [Microsoft][SQL Server Native Client 11.0][SQL Server]Violation of PRIMARY KEY constraint 'PK_adminlog_id'. 
Cannot insert duplicate key in object 'dbo.adminlog'. 
The duplicate key value is (F5CABD8F-E000-4677-8F5F-78B4CD3B9560). (2627) (SQLExecDirectW); 
[23000] [Microsoft][SQL Server Native Client 11.0][SQL Server]The statement has been terminated. (3621)") 
[SQL: 'INSERT INTO adminlog (
        alog_id, 
        alog_ppl_id, 
        alog_user_ppl_id, 
        alog_user_name, 
        alog_datetime, 
        [alog_ipAddress], 
        [alog_macAddress], 
        alog_comment, 
        alog_type, 
        alog_act_id, 
        alog_comp_id, 
        alog_artc_id) 
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'] 

        [parameters: ((
        b'F5CABD8F-E000-4677-8F5F-78B4CD3B9560', 
        b'0D10D3EF-F37E-45BE-8EED-B5987AE80732', 
        b'99999999-9999-9999-1111-999999999999', 
        'System', 
        datetime.datetime(2018, 10, 29, 13, 51, 30, 555495), 
        b'0.0.0.0', 
        b'4A-NO-NY-MO-US', 
        'Le service a été ajouté. Cours Coll (119,88)', 
        b'user', 
        None, 
        None, 
        None), 
        (
        b'39395ACA-0AFB-4C5F-90D4-0C6F95D7B8BC', 
        b'0D10D3EF-F37E-45BE-8EED-B5987AE80732', 
        b'99999999-9999-9999-1111-999999999999', 
        'System', 
        datetime.datetime(2018, 10, 29, 13, 51, 30, 777909), 
        b'0.0.0.0', 
        b'4A-NO-NY-MO-US', 
        'Le service a été supprimé. 01/12/2019 Cours Coll (119,88)', 
        b'user', 
        None, 
        None, 
        None)
        )]

У меня вопрос: существует ли автоматический процесс, который добавляет (изменяет) объекты в сеанс, без использования session.add()? Есть ли в SQLAlchemy возможность отключить это поведение и фиксировать сеанс только тогда, когда это явно сделано с использованием session.add(object)?

1 Ответ

0 голосов
/ 05 ноября 2018

У меня вопрос: есть ли автоматический процесс, который добавляет (изменяет) объекты в сеанс, без использования session.add()?

Существует по крайней мере одна функция, которая вытягивает объекты в Session без их явного добавления: save-update каскад . Когда объект добавляется в Session, все объекты, связанные с ним через атрибуты relationship(), для которых настроен этот каскад, также помещаются в Session. То же самое происходит также, когда объект связан с другим, который уже находится в Session.

Есть ли в SQLAlchemy возможность отключить это поведение и фиксировать сеанс только тогда, когда это явно сделано с помощью session.add(object)?

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

Если это так в вашем коде, то причина, по которой объекты добавляются дважды, заключается в том, что вы в некоторой степени явно это сделали. В массовых операциях пропущены большинство более продвинутых функций Session в пользу необработанной производительности - например, они не координируются с Session, если объект уже был сохранен, и при этом они не присоединяются, сохраняются объекты на Session:

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

Относительно того, почему проблема возникает в первую очередь, вам не нужно вручную держать «промежуточную область» - ваш составной объект - вокруг объектов. Это именно то, для чего Session в сочетании с правильным использованием транзакций. Функции и подфункции должны добавлять объекты в Session, когда это имеет смысл, но они не должны контролировать текущую транзакцию . Это должно происходить только вне вашей основной функции, где вы сейчас обрабатываете свой составной объект. Если вы сделаете откат, все изменения исчезнут.

При тестировании вы можете передать a Session, который присоединился к внешней транзакции , который будет явно откатан, независимо от того, что делает тестируемый код.

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