Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ошибка при использовании session.merge в методе new для модели User. #10

Open
Mat0mba24 opened this issue Jun 25, 2023 · 11 comments
Assignees
Labels
bug Something isn't working Classic

Comments

@Mat0mba24
Copy link

Mat0mba24 commented Jun 25, 2023

Использовал данный репозиторий и конкретно метод new для модели User.

Делал Middleware для пользователей, чтобы все пользователи фиксировались в БД после любого взаимодействия.

from typing import Any, Awaitable, Callable, Dict

from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, User
from sqlalchemy.ext.asyncio import async_sessionmaker

from db import Database


class AuthUserMiddleware(BaseMiddleware):
    async def __call__(
            self,
            handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
            event: TelegramObject,
            data: Dict[str, Any],
    ) -> Any:
        if not data.get("event_from_user"):
            return await handler(event, data)

        event_from_user: User = data["event_from_user"]
        session_maker: async_sessionmaker = data["session_maker"]

        async with session_maker() as session:
            db = Database(session)
            user = await db.user.new(
                user_id=event_from_user.id,
                user_name=event_from_user.username,
                first_name=event_from_user.first_name,
                second_name=event_from_user.last_name,
            )
            await session.commit()
            data["user"] = user

        return await handler(event, data)

После первого commit все нормально, пользователя сначала не существует - пользователь создается и передается дальше в Handler.

После второго и последующих commit проблема.

Потому что есть атрибут user_id, который должен быть уникальным, но он не определен как первичный ключ.
image

Merge смотрит по первичному ключу вроде, который есть у модели User как атрибут id от Base.
image

В общем, при втором и последующих commit возникает ошибка, мол, запись с таким user_id уже существует.

Возможно конкретно для модели User не стоит использовать merge? Или переопределить первичный ключ?

@MassonNN MassonNN added the bug Something isn't working label Jun 25, 2023
@MassonNN MassonNN self-assigned this Jun 25, 2023
@MassonNN
Copy link
Owner

Приложите, пожалуйста, полный traceback

@Mat0mba24
Copy link
Author

Приложите, пожалуйста, полный traceback

Traceback (most recent call last):

  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\aiogram\dispatcher\dispatcher.py", line 297, in _process_update
    response = await self.feed_update(bot, update, **kwargs)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\aiogram\dispatcher\dispatcher.py", line 146, in feed_update
    response = await self.update.wrap_outer_middleware(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\aiogram\dispatcher\middlewares\error.py", line 25, in __call__
    return await handler(event, data)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\aiogram\dispatcher\middlewares\user_context.py", line 23, in __call__
    return await handler(event, data)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\aiogram\fsm\middleware.py", line 39, in __call__
    return await handler(event, data)

  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\tgbot\middlewares\auth_user_mw.py", line 32, in __call__
    await session.commit()
          |       -> <function AsyncSession.commit at 0x00000293ACC9F880>
          -> <sqlalchemy.ext.asyncio.session.AsyncSession object at 0x00000293ACFE6530>

  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\ext\asyncio\session.py", line 944, in commit
    await greenlet_spawn(self.sync_session.commit)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 192, in greenlet_spawn
    result = context.switch(value)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 1920, in commit
    trans.commit(_to_root=True)
  File "<string>", line 2, in commit
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\state_changes.py", line 139, in _go
    ret_value = fn(self, *arg, **kw)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 1236, in commit
    self._prepare_impl()
  File "<string>", line 2, in _prepare_impl
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\state_changes.py", line 139, in _go
    ret_value = fn(self, *arg, **kw)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 1211, in _prepare_impl
    self.session.flush()
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 4163, in flush
    self._flush(objects)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 4298, in _flush
    with util.safe_reraise():
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\util\langhelpers.py", line 147, in __exit__
    raise exc_value.with_traceback(exc_tb)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\session.py", line 4259, in _flush
    flush_context.execute()
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 466, in execute
    rec.execute(self)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 642, in execute
    util.preloaded.orm_persistence.save_obj(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\persistence.py", line 93, in save_obj
    _emit_insert_statements(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\orm\persistence.py", line 1226, in _emit_insert_statements
    result = connection.execute(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1412, in execute
    return meth(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\sql\elements.py", line 483, in _execute_on_connection
    return connection._execute_clauseelement(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1635, in _execute_clauseelement
    ret = self._execute_context(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1844, in _execute_context
    return self._exec_single_context(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1984, in _exec_single_context
    self._handle_dbapi_exception(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 2339, in _handle_dbapi_exception
    raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1965, in _exec_single_context
    self.dialect.do_execute(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\engine\default.py", line 921, in do_execute
    cursor.execute(statement, parameters)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 561, in execute
    self._adapt_connection.await_(
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 125, in await_only
    return current.driver.switch(awaitable)  # type: ignore[no-any-return]
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 185, in greenlet_spawn
    value = await result
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 540, in _prepare_and_execute
    self._handle_exception(error)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 491, in _handle_exception
    self._adapt_connection._handle_exception(error)
  File "C:\Users\Admin\PycharmProjects\Bots_TG\Aiogram3\aiogram3_template\venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 778, in _handle_exception
    raise translated_error from error

sqlalchemy.exc.IntegrityError: (sqlalchemy.dialects.postgresql.asyncpg.IntegrityError) <class 'asyncpg.exceptions.UniqueViolationError'>: повторяющееся значение ключа нарушает ограничение уникальности "uq_users_user_id"
DETAIL:  Ключ "(user_id)=(892323156)" уже существует.
[SQL: INSERT INTO users (user_id, user_name, first_name, second_name, is_admin) VALUES ($1::BIGINT, $2::VARCHAR, $3::VARCHAR, $4::VARCHAR, $5::BOOLEAN) RETURNING users.id, users.created_at, users.updated_at]
[parameters: (892323156, 'mat0mba', 'Daniil', None, False)]
(Background on this error at: https://sqlalche.me/e/20/gkpj)

@Mat0mba24
Copy link
Author

Mat0mba24 commented Jun 26, 2023

@MassonNN я вот думаю, что это скорее даже не баг, в целом, метод new как раз и подразумевает только добавление новой записи в БД, но не обновление ее или возврат.

Просто merge хорошо будет использоваться в моем случае, если решить проблему с первичным ключом, но не знаю как.

@MassonNN
Copy link
Owner

Временное решение. Оберните вызов метода new в try except, ловите ошибку и в случае ее возникновения вызывайте update.

@Mat0mba24
Copy link
Author

Временное решение. Оберните вызов метода new в try except, ловите ошибку и в случае ее возникновения вызывайте update.

А вообще в качестве первичного ключа разумно использовать telegram user_id пользователя?
Вместо id от Base модели?
Не вызовет ли это проблем при индексировании?

@MassonNN
Copy link
Owner

А вообще в качестве первичного ключа разумно использовать telegram user_id пользователя?

Нет, user ID это информация, приходящая от телеграмма, вы не можете ей доверять на 100%

@Mat0mba24
Copy link
Author

Mat0mba24 commented Jun 26, 2023

Нет, user ID это информация, приходящая от телеграмма, вы не можете ей доверять на 100%

Тогда merge наверное стоит заменить на add в моем случае? Например, если не заинтересован в обновлении данных записи и делать проверку наличия записи в БД перед add.

Что-то такое?

async with session_maker() as session:
    db = Database(session)
    user = await db.user.get_user_by_tg_user_id(user_id=event_from_user.id)
    if not user:
        user = await db.user.new(
            user_id=event_from_user.id,
            user_name=event_from_user.username,
            first_name=event_from_user.first_name,
            second_name=event_from_user.last_name,
        )
    data["user"] = user
    return await handler(event, data)

@MassonNN
Copy link
Owner

Нет, user ID это информация, приходящая от телеграмма, вы не можете ей доверять на 100%

Тогда merge наверное стоит заменить на add в моем случае? Например, если не заинтересован в обновлении данных записи и делать проверку наличия записи в БД перед add.

Что-то такое?

async with session_maker() as session:
    db = Database(session)
    user = await db.user.get_user_by_tg_user_id(user_id=event_from_user.id)
    if not user:
        user = await db.user.new(
            user_id=event_from_user.id,
            user_name=event_from_user.username,
            first_name=event_from_user.first_name,
            second_name=event_from_user.last_name,
        )
    data["user"] = user
    return await handler(event, data)

пока что сделайте так, как я вам сказал, в будущем поменяю этот метод чтобы он обрабатывал такие места

@cyborg728
Copy link

saved_user = await self.session.execute(
            insert(User)
            .values(**kwargs)
            .on_conflict_do_update(
                index_elements=(User.tg_id,), set_=kwargs, where=User.tg_id == user.tg_id
            )
            .returning(User)
        )

Что-то типа такого

@MassonNN
Copy link
Owner

Мое личное мнение, что решение, которое предлагает @cyborg728, можно считать одним из лучших, но в то же время, метод merge не должен вести себя так как ведет в настоящем ищью.

Мое предложение - создать новый метод репозитория - add_or_update, с использованием решения от @cyborg728, оставив текущий метод add без изменений.

@cyborg728
Copy link

Мое личное мнение, что решение, которое предлагает @cyborg728, можно считать одним из лучших, но в то же время, метод merge не должен вести себя так как ведет в настоящем ищью.

Мое предложение - создать новый метод репозитория - add_or_update, с использованием решения от @cyborg728, оставив текущий метод add без изменений.

В случае с его мидлварью - get_or_create 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Classic
Projects
None yet
Development

No branches or pull requests

3 participants